mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-22 11:01:37 -05:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cacf0777c7 | ||
|
|
cb433fbeac | ||
|
|
8f6736df5e | ||
|
|
ef76d730e3 | ||
|
|
aabbeeffa9 | ||
|
|
7a513de543 | ||
|
|
abbd5ec037 | ||
|
|
84009f65ec | ||
|
|
191648df0a | ||
|
|
1265a56ff4 | ||
|
|
2171d44922 | ||
|
|
812f315e69 | ||
|
|
d48258a0fb | ||
|
|
8ec78f67af | ||
|
|
0ac0175bb1 | ||
|
|
f39007cd2d | ||
|
|
348b03327e | ||
|
|
2349aa4df8 | ||
|
|
6497bbcc38 | ||
|
|
a93a173183 | ||
|
|
a1abca26df | ||
|
|
42e55dd2dd | ||
|
|
ca3146d38f | ||
|
|
7f6cd514a5 | ||
|
|
88e24e92b5 | ||
|
|
8b447e361f | ||
|
|
a92dd2d4e4 | ||
|
|
852ec44567 | ||
|
|
b6e32cdeb4 | ||
|
|
4bd264d9be | ||
|
|
4a4b8574d0 | ||
|
|
ded073edd9 | ||
|
|
568f95e7a3 | ||
|
|
5adcabc8de | ||
|
|
e3bce7172c | ||
|
|
96fb618390 | ||
|
|
2d4fdcb661 | ||
|
|
7a84cff5b4 | ||
|
|
2af627b704 | ||
|
|
77fdf18686 | ||
|
|
944a77fe42 | ||
|
|
049678b32e | ||
|
|
3325e4d854 | ||
|
|
1e90e88d4b | ||
|
|
8aeb33c98c | ||
|
|
3b08721835 |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please search existing issues to avoid creating duplicates. -->
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution or feature you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -69,7 +69,7 @@ export class AlertData implements IAgentDialogData {
|
||||
this.eventDescriptionKeyword = alertInfo.eventDescriptionKeyword;
|
||||
this.eventSource = alertInfo.eventSource;
|
||||
this.hasNotification = alertInfo.hasNotification;
|
||||
this.includeEventDescription = alertInfo.includeEventDescription.toString();
|
||||
this.includeEventDescription = alertInfo.includeEventDescription ? alertInfo.includeEventDescription.toString() : null;
|
||||
this.isEnabled = alertInfo.isEnabled;
|
||||
this.jobId = alertInfo.jobId;
|
||||
this.lastOccurrenceDate = alertInfo.lastOccurrenceDate;
|
||||
@@ -82,7 +82,7 @@ export class AlertData implements IAgentDialogData {
|
||||
this.databaseName = alertInfo.databaseName;
|
||||
this.countResetDate = alertInfo.countResetDate;
|
||||
this.categoryName = alertInfo.categoryName;
|
||||
this.alertType = alertInfo.alertType.toString();
|
||||
this.alertType = alertInfo.alertType ? alertInfo.alertType.toString() : null;
|
||||
this.wmiEventNamespace = alertInfo.wmiEventNamespace;
|
||||
this.wmiEventQuery = alertInfo.wmiEventQuery;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,50 @@
|
||||
# Microsoft SQL Server Import for Azure Data Studio
|
||||
|
||||
Microsoft SQL Server Import for Azure Data Studio is a simple way to copy data from a flat file (.csv, .txt, .json) to a SQL Server table. Checkout below the reasons for using the Import Flat File wizard, how to find this wizard, and a simple example.
|
||||
Microsoft SQL Server Import for Azure Data Studio includes two wizards:
|
||||
- [Import Flat File Wizard](#import-flat-file-wizard-preview)
|
||||
- [Data-tier Application Wizard.](#data-tier-application-wizard-preview)
|
||||
|
||||
## Import Flat File Wizard *(preview)*
|
||||
**The Import Flat File Wizard** is a simple way to copy data from a flat file (.csv, .txt, .json) to a SQL Server table. Checkout below the reasons for using the Import Flat File wizard, how to find this wizard, and a simple example.
|
||||
|
||||
This experience is currently in its initial preview. Please report issues and feature requests [here.](https://github.com/microsoft/azuredatastudio/issues)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/30873802/43433347-c958ed28-942b-11e8-8bbc-f4f2529c3978.png" width="800px" />
|
||||
|
||||
### Requirements
|
||||
* This wizard requires an active connection to a SQL Server instance to start.
|
||||
* This wizard only works on .txt and .csv files.
|
||||
|
||||
### How do I start the Import Flat File wizard?
|
||||
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Import wizard**.
|
||||
* If a user is connected to a SQL Server instance, the user can also press **Ctrl**+**I** to start the wizard.
|
||||
|
||||
### Why would I use the Import Flat File wizard?
|
||||
This wizard was created to improve the current import experience leveraging an intelligent framework known as Program Synthesis using Examples ([PROSE](https://microsoft.github.io/prose/)). For a user without specialized domain knowledge, importing data can often be a complex, error prone, and tedious task. This wizard streamlines the import process as simple as selecting an input file and unique table name, and the PROSE framework handles the rest.
|
||||
|
||||
PROSE analyzes data patterns in your input file to infer column names, types, delimiters, and more. This framework learns the structure of the file and does all of the hard work so users don't have to.
|
||||
|
||||
Please note that the PROSE binary components used by this extension are licensed under the [MICROSOFT SQL TOOLS IMPORT FLAT FILE EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/extensions/import/Microsoft_SQL_Server_Import_Extension_and_Tools_Import_Flat_File_Preview.docx).
|
||||
|
||||
## Data-tier Application Wizard *(preview)*
|
||||
**The Data-tier Application Wizard** provides an easy to use experience to deploy and extract .dacpac files and import and export .bacpac files.
|
||||
|
||||
This experience is currently in its initial preview. Please report issues and feature requests [here.](https://github.com/microsoft/azuredatastudio/issues)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/30873802/49676289-f2df6880-fa2d-11e8-8bfa-6213b7734075.png" width="800px" />
|
||||
|
||||
### Requirements
|
||||
* This wizard requires an active connection to a SQL Server instance to start.
|
||||
|
||||
### How do I start the Data-tier Application wizard?
|
||||
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Data-tier Application wizard**.
|
||||
* If a user is connected to a SQL Server instance, the user can also start the wizard from the command palette (Ctrl+Shift+P) by searching for **Data-tier Application wizard.**
|
||||
|
||||
### Why would I use the Data-tier Application wizard?
|
||||
This wizard was created to add the ability to extract and deploy .dacpac files and import and export .bacpac files in Azure Data Studio.
|
||||
|
||||
To learn more about Data-Tier Applications and working with dacpac and bacpac files, [you can read more here.](https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications?view=sql-server-2017)
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
@@ -12,21 +53,6 @@ Licensed under the [MICROSOFT SQL SERVER IMPORT EXTENSION EULA](https://raw.gith
|
||||
|
||||
> Note: Microsoft SQL Server Import for Azure Data Studio extension contains the Microsoft SQL Tools Import Flat File component which is also licensed under the above EULA.
|
||||
|
||||
## Requirements
|
||||
* This wizard requires an active connection to a SQL Server instance to start.
|
||||
* This wizard only works on .txt and .csv files.
|
||||
|
||||
## How do I start the Flat File Import wizard?
|
||||
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Import wizard**.
|
||||
* If a user is connected to a SQL Server instance, the user can also press **Ctrl**+**I** to start the wizard.
|
||||
|
||||
## Why would I use the Flat File Import wizard?
|
||||
This wizard was created to improve the current import experience leveraging an intelligent framework known as Program Synthesis using Examples ([PROSE](https://microsoft.github.io/prose/)). For a user without specialized domain knowledge, importing data can often be a complex, error prone, and tedious task. This wizard streamlines the import process as simple as selecting an input file and unique table name, and the PROSE framework handles the rest.
|
||||
|
||||
PROSE analyzes data patterns in your input file to infer column names, types, delimiters, and more. This framework learns the structure of the file and does all of the hard work so users don't have to.
|
||||
|
||||
Please note that the PROSE binary components used by this extension are licensed under the [MICROSOFT SQL TOOLS IMPORT FLAT FILE EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/extensions/import/Microsoft_SQL_Server_Import_Extension_and_Tools_Import_Flat_File_Preview.docx).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
@@ -202,7 +202,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async deploy() {
|
||||
let service = await DataTierApplicationWizard.getService();
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||
@@ -213,7 +213,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async extract() {
|
||||
let service = await DataTierApplicationWizard.getService();
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||
@@ -224,7 +224,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async export() {
|
||||
let service = await DataTierApplicationWizard.getService();
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||
@@ -235,7 +235,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async import() {
|
||||
let service = await DataTierApplicationWizard.getService();
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||
@@ -245,9 +245,8 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
}
|
||||
|
||||
public static async getService(): Promise<sqlops.DacFxServicesProvider> {
|
||||
let currentConnection = await sqlops.connection.getCurrentConnection();
|
||||
let service = sqlops.dataprotocol.getProvider<sqlops.DacFxServicesProvider>(currentConnection.providerName, sqlops.DataProviderType.DacFxServicesProvider);
|
||||
private static async getService(providerName: string): Promise<sqlops.DacFxServicesProvider> {
|
||||
let service = sqlops.dataprotocol.getProvider<sqlops.DacFxServicesProvider>(providerName, sqlops.DataProviderType.DacFxServicesProvider);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
}
|
||||
|
||||
async start(): Promise<boolean> {
|
||||
let serverComponent = await this.createServerDropdown(true);
|
||||
let serverComponent = await this.createServerDropdown(true);
|
||||
let fileBrowserComponent = await this.createFileBrowser();
|
||||
this.databaseComponent = await this.createDatabaseTextBox();
|
||||
this.databaseComponent.title = localize('dacFx.databaseNameTextBox', 'Database Name');
|
||||
@@ -131,7 +131,7 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
this.model.database = this.databaseTextBox.value;
|
||||
});
|
||||
|
||||
// Initialize with upgrade existing true
|
||||
//Initialize with upgrade existing true
|
||||
upgradeRadioButton.checked = true;
|
||||
this.model.upgradeExisting = true;
|
||||
|
||||
@@ -149,10 +149,10 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
}
|
||||
|
||||
protected async createDeployDatabaseDropdown(): Promise<sqlops.FormComponent> {
|
||||
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
|
||||
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
|
||||
required: true
|
||||
}).component();
|
||||
// Handle database changes
|
||||
//Handle database changes
|
||||
this.databaseDropdown.onValueChanged(async () => {
|
||||
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
|
||||
});
|
||||
@@ -172,7 +172,8 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
}
|
||||
let values = await this.getDatabaseValues();
|
||||
|
||||
if (this.model.database === undefined) {
|
||||
//set the database to the first dropdown value if upgrading, otherwise it should get set to the textbox value
|
||||
if (this.model.upgradeExisting) {
|
||||
this.model.database = values[0].name;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "azuredatastudio",
|
||||
"version": "1.3.7",
|
||||
"version": "1.3.9",
|
||||
"distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
"Microsoft.server-report",
|
||||
"Microsoft.sql-vnext",
|
||||
"Microsoft.whoisactive",
|
||||
"Redgate.sql-search"
|
||||
"Redgate.sql-search",
|
||||
"IDERA.sqldm-performance-insights"
|
||||
],
|
||||
"extensionsGallery": {
|
||||
"serviceUrl": "https://sqlopsextensions.blob.core.windows.net/marketplace/v1/extensionsGallery.json"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
|
||||
export interface IObservableCollection<T> {
|
||||
getLength(): number;
|
||||
@@ -14,16 +15,12 @@ export interface IObservableCollection<T> {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
class LoadCancellationToken {
|
||||
isCancelled: boolean;
|
||||
}
|
||||
|
||||
class DataWindow<T> {
|
||||
private _data: T[];
|
||||
private _length: number = 0;
|
||||
private _offsetFromDataSource: number = -1;
|
||||
|
||||
private lastLoadCancellationToken: LoadCancellationToken;
|
||||
private cancellationToken = new CancellationTokenSource();
|
||||
|
||||
constructor(
|
||||
private loadFunction: (offset: number, count: number) => Thenable<T[]>,
|
||||
@@ -36,9 +33,7 @@ class DataWindow<T> {
|
||||
this.loadFunction = undefined;
|
||||
this.placeholderItemGenerator = undefined;
|
||||
this.loadCompleteCallback = undefined;
|
||||
if (this.lastLoadCancellationToken) {
|
||||
this.lastLoadCancellationToken.isCancelled = true;
|
||||
}
|
||||
this.cancellationToken.cancel();
|
||||
}
|
||||
|
||||
public getStartIndex(): number {
|
||||
@@ -65,17 +60,16 @@ class DataWindow<T> {
|
||||
this._length = length;
|
||||
this._data = undefined;
|
||||
|
||||
if (this.lastLoadCancellationToken) {
|
||||
this.lastLoadCancellationToken.isCancelled = true;
|
||||
}
|
||||
this.cancellationToken.cancel();
|
||||
this.cancellationToken = new CancellationTokenSource();
|
||||
const currentCancellation = this.cancellationToken;
|
||||
|
||||
if (length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastLoadCancellationToken = new LoadCancellationToken();
|
||||
this.loadFunction(offset, length).then(data => {
|
||||
if (!this.lastLoadCancellationToken.isCancelled) {
|
||||
if (!currentCancellation.token.isCancellationRequested) {
|
||||
this._data = data;
|
||||
this.loadCompleteCallback(this._offsetFromDataSource, this._offsetFromDataSource + this._length);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
||||
this._onRowCountChange.fire();
|
||||
}
|
||||
|
||||
find(exp: string): Thenable<IFindPosition> {
|
||||
find(exp: string, maxMatches: number = 0): Thenable<IFindPosition> {
|
||||
if (!this._findFn) {
|
||||
return TPromise.wrapError(new Error('no find function provided'));
|
||||
}
|
||||
@@ -87,7 +87,8 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
||||
this._onFindCountChange.fire(this._findArray.length);
|
||||
if (exp) {
|
||||
this._findObs = Observable.create((observer: Observer<IFindPosition>) => {
|
||||
this._data.forEach((item, i) => {
|
||||
for (let i = 0; i < this._data.length; i++) {
|
||||
let item = this._data[i];
|
||||
let result = this._findFn(item, exp);
|
||||
if (result) {
|
||||
result.forEach(pos => {
|
||||
@@ -96,8 +97,11 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
||||
observer.next(index);
|
||||
this._onFindCountChange.fire(this._findArray.length);
|
||||
});
|
||||
if (maxMatches > 0 && this._findArray.length > maxMatches) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return this._findObs.take(1).toPromise().then(() => {
|
||||
return this._findArray[this._findIndex];
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorInput, IEditorInput } from 'vs/workbench/common/editor';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
|
||||
import URI from 'vs/base/common/uri';
|
||||
@@ -17,9 +16,9 @@ import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService';
|
||||
import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
|
||||
import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput';
|
||||
import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry';
|
||||
import { DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
|
||||
import { DEFAULT_NOTEBOOK_PROVIDER, INotebookService } from 'sql/services/notebook/notebookService';
|
||||
import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils';
|
||||
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -30,6 +29,7 @@ export const untitledFilePrefix = 'SQLQuery';
|
||||
|
||||
// mode identifier for SQL mode
|
||||
export const sqlModeId = 'sql';
|
||||
export const notebookModeId = 'notebook';
|
||||
|
||||
/**
|
||||
* Checks if the specified input is supported by one our custom input types, and if so convert it
|
||||
@@ -59,20 +59,20 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti
|
||||
|
||||
//Notebook
|
||||
let notebookValidator = instantiationService.createInstance(NotebookInputValidator);
|
||||
uri = getNotebookEditorUri(input);
|
||||
if(uri && notebookValidator.isNotebookEnabled()){
|
||||
//TODO: We need to pass in notebook data either through notebook input or notebook service
|
||||
let fileName: string = 'untitled';
|
||||
let providerId: string = DEFAULT_NOTEBOOK_PROVIDER;
|
||||
if (input) {
|
||||
fileName = input.getName();
|
||||
providerId = getProviderForFileName(fileName);
|
||||
}
|
||||
let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined);
|
||||
notebookInputModel.providerId = providerId;
|
||||
//TO DO: Second parameter has to be the content.
|
||||
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
|
||||
return notebookInput;
|
||||
uri = getNotebookEditorUri(input, instantiationService);
|
||||
if (uri && notebookValidator.isNotebookEnabled()) {
|
||||
return withService<INotebookService, NotebookInput>(instantiationService, INotebookService, notebookService => {
|
||||
let fileName: string = 'untitled';
|
||||
let providerId: string = DEFAULT_NOTEBOOK_PROVIDER;
|
||||
if (input) {
|
||||
fileName = input.getName();
|
||||
providerId = getProviderForFileName(fileName, notebookService);
|
||||
}
|
||||
let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined);
|
||||
notebookInputModel.providerId = providerId;
|
||||
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
|
||||
return notebookInput;
|
||||
});
|
||||
}
|
||||
}
|
||||
return input;
|
||||
@@ -97,6 +97,13 @@ export function getSupportedInputResource(input: IEditorInput): URI {
|
||||
}
|
||||
}
|
||||
|
||||
if (input instanceof ResourceEditorInput) {
|
||||
let resourceCast: ResourceEditorInput = <ResourceEditorInput>input;
|
||||
if (resourceCast) {
|
||||
return resourceCast.getResource();
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -154,34 +161,50 @@ function getQueryPlanEditorUri(input: EditorInput): URI {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If input is a supported notebook editor file (.ipynb), return it's URI. Otherwise return undefined.
|
||||
* @param input The EditorInput to get the URI of.
|
||||
*/
|
||||
function getNotebookEditorUri(input: EditorInput): URI {
|
||||
function getNotebookEditorUri(input: EditorInput, instantiationService: IInstantiationService): URI {
|
||||
if (!input || !input.getName()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// If this editor is not already of type notebook input
|
||||
if (!(input instanceof NotebookInput)) {
|
||||
let uri: URI = getSupportedInputResource(input);
|
||||
if (uri) {
|
||||
if (hasFileExtension(getNotebookFileExtensions(), input, false)) {
|
||||
if (hasFileExtension(getNotebookFileExtensions(instantiationService), input, false) || hasNotebookFileMode(input)) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getNotebookFileExtensions() {
|
||||
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
||||
return notebookRegistry.getSupportedFileExtensions();
|
||||
function getNotebookFileExtensions(instantiationService: IInstantiationService): string[] {
|
||||
return withService<INotebookService, string[]>(instantiationService, INotebookService, notebookService => {
|
||||
return notebookService.getSupportedFileExtensions();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given EditorInput is set to either undefined or notebook mode
|
||||
* @param input The EditorInput to check the mode of
|
||||
*/
|
||||
function hasNotebookFileMode(input: EditorInput): boolean {
|
||||
if (input instanceof UntitledEditorInput) {
|
||||
let untitledCast: UntitledEditorInput = <UntitledEditorInput>input;
|
||||
return (untitledCast && untitledCast.getModeId() === notebookModeId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function withService<TService, TResult>(instantiationService: IInstantiationService, serviceId: ServiceIdentifier<TService>, action: (service: TService) => TResult, ): TResult {
|
||||
return instantiationService.invokeFunction(accessor => {
|
||||
let service = accessor.get(serviceId);
|
||||
return action(service);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,3 +242,17 @@ function hasFileExtension(extensions: string[], input: EditorInput, checkUntitle
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns file mode - notebookModeId or sqlModeId
|
||||
export function getFileMode(instantiationService: IInstantiationService, resource: URI): string {
|
||||
if (!resource) {
|
||||
return sqlModeId;
|
||||
}
|
||||
return withService<INotebookService, string>(instantiationService, INotebookService, notebookService => {
|
||||
for (const editor of notebookService.listNotebookEditors()) {
|
||||
if (editor.notebookParams.notebookUri === resource) {
|
||||
return notebookModeId;
|
||||
}
|
||||
}
|
||||
return sqlModeId;
|
||||
});
|
||||
}
|
||||
@@ -90,6 +90,8 @@
|
||||
margin: 0px 13px;
|
||||
}
|
||||
|
||||
.vs-dark .connection-dialog .connection-history-actions .action-label.icon,
|
||||
.hc-black .connection-dialog .connection-history-actions .action-label.icon,
|
||||
.connection-dialog .connection-history-actions .action-label.icon {
|
||||
display: block;
|
||||
height: 20px;
|
||||
@@ -105,10 +107,12 @@
|
||||
background: url('clear-search-results.svg');
|
||||
}
|
||||
|
||||
/*
|
||||
.vs-dark .search-action.clear-search-results,
|
||||
.hc-black .search-action.clear-search-results {
|
||||
background: url('clear-search-results-dark.svg');
|
||||
}
|
||||
*/
|
||||
|
||||
.connection-details-title {
|
||||
font-size: 14px;
|
||||
|
||||
@@ -168,8 +168,7 @@ export const DashboardModule = (params, selector: string, instantiationService:
|
||||
if (e instanceof NavigationEnd) {
|
||||
this.navigations++;
|
||||
TelemetryUtils.addTelemetry(this.telemetryService, TelemetryKeys.DashboardNavigated, {
|
||||
numberOfNavigations: this.navigations,
|
||||
routeUrl: e.url
|
||||
numberOfNavigations: this.navigations
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,13 +48,13 @@ export function textFormatter(row: number, cell: any, value: any, columnDef: any
|
||||
if (!value.isNull) {
|
||||
valueToDisplay = value.displayValue.replace(/(\r\n|\n|\r)/g, ' ');
|
||||
valueToDisplay = escape(valueToDisplay.length > 250 ? valueToDisplay.slice(0, 250) + '...' : valueToDisplay);
|
||||
titleValue = value.displayValue;
|
||||
titleValue = valueToDisplay;
|
||||
} else {
|
||||
cellClasses += ' missing-value';
|
||||
}
|
||||
} else if (typeof value === 'string') {
|
||||
valueToDisplay = escape(value.length > 250 ? value.slice(0, 250) + '...' : value);
|
||||
titleValue = value;
|
||||
titleValue = valueToDisplay;
|
||||
}
|
||||
|
||||
return `<span title="${titleValue}" class="${cellClasses}">${valueToDisplay}</span>`;
|
||||
|
||||
@@ -343,6 +343,9 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
|
||||
handleResultSet(self: EditDataComponent, event: any): void {
|
||||
// Clone the data before altering it to avoid impacting other subscribers
|
||||
let resultSet = Object.assign({}, event.data);
|
||||
if (!resultSet.complete) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add an extra 'new row'
|
||||
resultSet.rowCount++;
|
||||
|
||||
@@ -304,14 +304,6 @@ table.jobprevruns > tbody {
|
||||
background-image: url('refresh_inverse.svg');
|
||||
}
|
||||
|
||||
.agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
jobsview-component .agent-actionbar-container {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.agent-actionbar-container .monaco-action-bar > ul.actions-container > li.action-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
@@ -414,4 +406,11 @@ jobsview-component .jobview-grid .slick-cell.error-row {
|
||||
#proxiesDiv .proxyview-proxynameindicatordisabled {
|
||||
width: 5px;
|
||||
background: red;
|
||||
}
|
||||
|
||||
#jobsDiv jobsview-component .monaco-toolbar.carbon-taskbar,
|
||||
#operatorsDiv joboperatorsview-component .monaco-toolbar.carbon-taskbar,
|
||||
#alertsDiv jobalertsview-component .monaco-toolbar.carbon-taskbar,
|
||||
#proxiesDiv jobproxiesview-component .monaco-toolbar.carbon-taskbar {
|
||||
margin: 10px 0px 10px 0px;
|
||||
}
|
||||
@@ -156,8 +156,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div #jobsteps style="flex: 1 1 auto; position: relative">
|
||||
<jobstepsview-component *ngIf="showSteps === true" style="position: absolute; height: 100%; width: 100%"></jobstepsview-component>
|
||||
<div #jobsteps *ngIf="showSteps === true" style="flex: 1 1 auto; position: relative">
|
||||
<jobstepsview-component *ngIf="showSteps === true"></jobstepsview-component>
|
||||
</div>
|
||||
<h3 *ngIf="showSteps === false">No Steps Available</h3>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,6 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
|
||||
private _agentJobInfo: sqlops.AgentJobInfo;
|
||||
private _noJobsAvailable: boolean = false;
|
||||
|
||||
private static readonly INITIAL_TREE_HEIGHT: number = 780;
|
||||
private static readonly HEADING_HEIGHT: number = 24;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
.all-jobs {
|
||||
display: inline;
|
||||
font-size: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.overview-container .overview-tab .resultsViewCollapsible {
|
||||
@@ -266,14 +265,22 @@ jobhistory-component > .jobhistory-heading-container > .icon.in-progress {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
||||
jobhistory-component > .agent-actionbar-container {
|
||||
border-top: 3px solid #f4f4f4;
|
||||
}
|
||||
|
||||
.vs-dark jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
||||
.vs-dark jobhistory-component > .agent-actionbar-container {
|
||||
border-top: 3px solid #444444;
|
||||
}
|
||||
|
||||
.hc-black jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
||||
.hc-black jobhistory-component > .agent-actionbar-container {
|
||||
border-top: 3px solid #2b56f2;
|
||||
}
|
||||
|
||||
jobhistory-component .step-table.prev-run-list .monaco-tree-wrapper .monaco-tree-row {
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
jobhistory-component .agent-actionbar-container > .monaco-toolbar.carbon-taskbar {
|
||||
margin: 10px 0px 5px 0px;
|
||||
}
|
||||
@@ -80,4 +80,11 @@
|
||||
jobstepsview-component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row {
|
||||
width: 99.2%;
|
||||
}
|
||||
@@ -591,7 +591,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
||||
|
||||
private async curateJobHistory(jobs: sqlops.AgentJobInfo[], ownerUri: string) {
|
||||
const self = this;
|
||||
jobs.forEach(async (job) => {
|
||||
await Promise.all(jobs.map(async (job) => {
|
||||
await this._jobManagementService.getJobHistory(ownerUri, job.jobId, job.name).then(async(result) => {
|
||||
if (result) {
|
||||
self.jobSteps[job.jobId] = result.steps ? result.steps : [];
|
||||
@@ -622,32 +622,23 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private createJobChart(jobId: string, jobHistories: sqlops.AgentJobHistoryInfo[]): void {
|
||||
let chartHeights = this.getChartHeights(jobHistories);
|
||||
let runCharts = [];
|
||||
for (let i = 0; i < jobHistories.length; i++) {
|
||||
for (let i = 0; i < chartHeights.length; i++) {
|
||||
let runGraph = $(`table#${jobId}.jobprevruns > tbody > tr > td > div.bar${i}`);
|
||||
if (jobHistories && jobHistories.length > 0) {
|
||||
runGraph.css('height', chartHeights[i]);
|
||||
let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green';
|
||||
runGraph.css('background', bgColor);
|
||||
runGraph.hover((e) => {
|
||||
let currentTarget = e.currentTarget;
|
||||
currentTarget.title = jobHistories[i].runDuration;
|
||||
});
|
||||
if (runGraph.get(0)) {
|
||||
runCharts.push(runGraph.get(0).outerHTML);
|
||||
}
|
||||
} else {
|
||||
runGraph.css('height', '5px');
|
||||
runGraph.css('background', 'red');
|
||||
runGraph.hover((e) => {
|
||||
let currentTarget = e.currentTarget;
|
||||
currentTarget.title = 'Job not run.';
|
||||
});
|
||||
runGraph.css('height', chartHeights[i]);
|
||||
let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green';
|
||||
runGraph.css('background', bgColor);
|
||||
runGraph.hover((e) => {
|
||||
let currentTarget = e.currentTarget;
|
||||
currentTarget.title = jobHistories[i].runDuration;
|
||||
});
|
||||
if (runGraph.get(0)) {
|
||||
runCharts.push(runGraph.get(0).outerHTML);
|
||||
}
|
||||
}
|
||||
if (runCharts.length > 0) {
|
||||
@@ -658,7 +649,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
||||
// chart height normalization logic
|
||||
private getChartHeights(jobHistories: sqlops.AgentJobHistoryInfo[]): string[] {
|
||||
if (!jobHistories || jobHistories.length === 0) {
|
||||
return ['5px', '5px', '5px', '5px', '5px'];
|
||||
return [];
|
||||
}
|
||||
let maxDuration: number = 0;
|
||||
jobHistories.forEach(history => {
|
||||
|
||||
@@ -2,31 +2,32 @@
|
||||
<span *ngIf="hasStatus" class="card-status">
|
||||
<div class="status-content" [style.backgroundColor]="statusColor"></div>
|
||||
</span>
|
||||
|
||||
<ng-container *ngIf="isVerticalButton">
|
||||
<div class="card-vertical-button">
|
||||
<div *ngIf="iconPath" class="iconContainer">
|
||||
<div [class]="iconClass" [style.maxWidth]="iconWidth" [style.maxHeight]="iconHeight"></div>
|
||||
</div>
|
||||
<h4 class="card-label">{{label}}</h4>
|
||||
<span *ngIf="showRadioButton" class="selection-indicator-container">
|
||||
<div *ngIf="showAsSelected" class="selection-indicator"></div>
|
||||
</span>
|
||||
<ng-container *ngIf="isVerticalButton">
|
||||
<div class="card-vertical-button">
|
||||
<div *ngIf="iconPath" class="iconContainer">
|
||||
<div [class]="iconClass" [style.maxWidth]="iconWidth" [style.maxHeight]="iconHeight"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isDetailsCard">
|
||||
<div class="card-content">
|
||||
<h4 class="card-label">{{label}}</h4>
|
||||
<p class="card-value">{{value}}</p>
|
||||
<span *ngIf="actions">
|
||||
<table class="model-table">
|
||||
<tr *ngFor="let action of actions">
|
||||
<td class="table-row">{{action.label}}</td>
|
||||
<td *ngIf="action.actionTitle" class="table-row">
|
||||
<a class="pointer prominent" (click)="onDidActionClick(action)">{{action.actionTitle}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h4 class="card-label">{{label}}</h4>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isDetailsCard">
|
||||
<div class="card-content">
|
||||
<h4 class="card-label">{{label}}</h4>
|
||||
<p class="card-value">{{value}}</p>
|
||||
<span *ngIf="actions">
|
||||
<table class="model-table">
|
||||
<tr *ngFor="let action of actions">
|
||||
<td class="table-row">{{action.label}}</td>
|
||||
<td *ngIf="action.actionTitle" class="table-row">
|
||||
<a class="pointer prominent" (click)="onDidActionClick(action)">{{action.actionTitle}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -29,7 +29,7 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
|
||||
|
||||
private backgroundColor: string;
|
||||
|
||||
constructor( @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
|
||||
constructor(@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
|
||||
) {
|
||||
@@ -130,6 +130,14 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
|
||||
return this.cardType === 'VerticalButton';
|
||||
}
|
||||
|
||||
public get showRadioButton():boolean{
|
||||
return this.selectable && (this.selected || this._hasFocus)
|
||||
}
|
||||
|
||||
public get showAsSelected(): boolean {
|
||||
return this.selectable && this.selected;
|
||||
}
|
||||
|
||||
|
||||
public get actions(): ActionDescriptor[] {
|
||||
return this.getPropertyOrDefault<CardProperties, ActionDescriptor[]>((props) => props.actions, []);
|
||||
@@ -156,6 +164,7 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
|
||||
|
||||
private updateTheme(theme: IColorTheme) {
|
||||
this.backgroundColor = theme.getColor(colors.editorBackground, true).toString();
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private onDidActionClick(action: ActionDescriptor): void {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
.model-card {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -7,23 +6,18 @@
|
||||
margin: 15px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
box-shadow: rgba(120, 120, 120, 0.75) 0px 0px 6px;
|
||||
}
|
||||
|
||||
.model-card.selected {
|
||||
border-color: darkblue
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .model-card.selected,
|
||||
.hc-black .monaco-workbench .model-card.selected {
|
||||
border-color: darkblue
|
||||
border-color: rgb(0, 120, 215);
|
||||
box-shadow: rgba(0, 120, 215, 0.75) 0px 0px 6px;
|
||||
}
|
||||
|
||||
.model-card.unselected {
|
||||
border-color: rgb(214, 214, 214);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,21 +96,43 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.model-card .selection-indicator-container {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
overflow: hidden;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
border-width: 1px;
|
||||
border-color: rgb(0, 120, 215);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.model-card .selection-indicator {
|
||||
margin: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: rgb(0, 120, 215);
|
||||
}
|
||||
|
||||
.model-card .model-table {
|
||||
border-spacing: 5px;
|
||||
}
|
||||
|
||||
.model-table .table-row {
|
||||
width: auto;
|
||||
clear: both;
|
||||
width: auto;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.model-table .table-cell {
|
||||
vertical-align: top;
|
||||
padding: 7px;
|
||||
vertical-align: top;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.model-table a {
|
||||
cursor: pointer;
|
||||
text-decoration: underline
|
||||
}
|
||||
}
|
||||
127
src/sql/parts/notebook/cellToggleMoreActions.ts
Normal file
127
src/sql/parts/notebook/cellToggleMoreActions.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ElementRef } from '@angular/core';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
|
||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { CellContext, CellActionBase } from 'sql/parts/notebook/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
|
||||
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { CellModel } from 'sql/parts/notebook/models/cell';
|
||||
|
||||
export class CellToggleMoreActions {
|
||||
private _actions: Action[] = [];
|
||||
private _moreActions: ActionBar;
|
||||
constructor(
|
||||
@IInstantiationService private instantiationService: IInstantiationService) {
|
||||
this._actions.push(
|
||||
instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete')),
|
||||
instantiationService.createInstance(AddCellFromContextAction,'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true),
|
||||
instantiationService.createInstance(ClearCellOutputAction, 'clear', localize('clear', 'Clear output'))
|
||||
);
|
||||
}
|
||||
|
||||
public toggle(showIcon: boolean, elementRef: ElementRef, model: NotebookModel, cellModel: ICellModel) {
|
||||
let context = new CellContext(model,cellModel);
|
||||
let moreActionsElement = <HTMLElement>elementRef.nativeElement;
|
||||
if (showIcon) {
|
||||
if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
}
|
||||
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
||||
this._moreActions.context = { target: moreActionsElement };
|
||||
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, context), { icon: showIcon, label: false });
|
||||
}
|
||||
else if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AddCellFromContextAction extends CellActionBase {
|
||||
constructor(
|
||||
id: string, label: string, private cellType: CellType, private isAfter: boolean,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let model = context.model;
|
||||
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
|
||||
if (index !== undefined && this.isAfter) {
|
||||
index += 1;
|
||||
}
|
||||
model.addCell(this.cellType, index);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCellAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
context.model.deleteCell(context.cell);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class ClearCellOutputAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
(context.model.activeCell as CellModel).clearOutputs();
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,6 @@
|
||||
</div>
|
||||
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
|
||||
</div>
|
||||
<div #moreactions class="toolbar" style="flex: 0 0 auto; display: flex; flex-flow:column; width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
|
||||
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!./code';
|
||||
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
||||
import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor';
|
||||
import { CellToggleMoreActions } from 'sql/parts/notebook/cellToggleMoreActions';
|
||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { RunCellAction, CellContext } from 'sql/parts/notebook/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
@@ -19,20 +24,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { RunCellAction, DeleteCellAction, AddCellAction, CellContext } from 'sql/parts/notebook/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
|
||||
import { CellTypes } from 'sql/parts/notebook/models/contracts';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
export const CODE_SELECTOR: string = 'code-component';
|
||||
@@ -59,15 +55,14 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
}
|
||||
|
||||
protected _actionBar: Taskbar;
|
||||
protected _moreActions: ActionBar;
|
||||
private readonly _minimumHeight = 30;
|
||||
private _editor: QueryTextEditor;
|
||||
private _editorInput: UntitledEditorInput;
|
||||
private _editorModel: ITextModel;
|
||||
private _uri: string;
|
||||
private _model: NotebookModel;
|
||||
private _actions: Action[] = [];
|
||||
private _activeCellId: string;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||
@@ -81,13 +76,7 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
@Inject(INotificationService) private notificationService: INotificationService,
|
||||
) {
|
||||
super();
|
||||
this._actions.push(
|
||||
this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false),
|
||||
this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true),
|
||||
this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false),
|
||||
this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true),
|
||||
this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete'))
|
||||
);
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -105,10 +94,10 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
if (propName === 'activeCellId') {
|
||||
let changedProp = changes[propName];
|
||||
if (this.cellModel.id === changedProp.currentValue) {
|
||||
this.toggleMoreActions(true);
|
||||
this._cellToggleMoreActions.toggle(true, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
else {
|
||||
this.toggleMoreActions(false);
|
||||
this._cellToggleMoreActions.toggle(false, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -173,21 +162,6 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
]);
|
||||
}
|
||||
|
||||
private toggleMoreActions(showIcon: boolean) {
|
||||
let context = new CellContext(this.model, this.cellModel);
|
||||
let moreActionsElement = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
||||
if (showIcon) {
|
||||
if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
}
|
||||
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
||||
this._moreActions.context = { target: moreActionsElement };
|
||||
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, context), { icon: showIcon, label: false });
|
||||
}
|
||||
else if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private createUri(): URI {
|
||||
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.cellModel.id}` });
|
||||
|
||||
@@ -45,10 +45,6 @@ code-component .carbon-taskbar .icon {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
code-component .action-label.icon.toggle-more {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ export class CellContext {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CellActionBase extends Action {
|
||||
export abstract class CellActionBase extends Action {
|
||||
|
||||
constructor(id: string, label: string, icon: string, protected notificationService: INotificationService) {
|
||||
super(id, label, icon);
|
||||
@@ -135,53 +135,3 @@ export class RunCellAction extends ToggleableAction {
|
||||
return clientSession.kernel;
|
||||
}
|
||||
}
|
||||
|
||||
export class AddCellAction extends CellActionBase {
|
||||
constructor(
|
||||
id: string, label: string, private cellType: CellType, private isAfter: boolean,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let model = context.model;
|
||||
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
|
||||
if (index !== undefined && this.isAfter) {
|
||||
index += 1;
|
||||
}
|
||||
model.addCell(this.cellType, index);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCellAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
context.model.deleteCell(context.cell);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,15 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<loading-spinner [loading]="isLoading"></loading-spinner>
|
||||
<div class="notebook-text" style="flex: 0 0 auto;">
|
||||
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [model]="model" [activeCellId]="activeCellId" [hideVerticalToolbar]=true>
|
||||
</code-component>
|
||||
</div>
|
||||
<div #preview style="flex: 0 0 auto;" (dblclick)="toggleEditMode()">
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
|
||||
<div #preview class ="notebook-preview" style="flex: 1 1 auto; user-select: initial;" (dblclick)="toggleEditMode()">
|
||||
</div>
|
||||
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,16 +6,21 @@ import 'vs/css!./textCell';
|
||||
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, OnChanges, SimpleChange } from '@angular/core';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { ISanitizer, defaultSanitizer } from 'sql/parts/notebook/outputs/sanitizer';
|
||||
import { localize } from 'vs/nls';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { CellToggleMoreActions } from 'sql/parts/notebook/cellToggleMoreActions';
|
||||
|
||||
export const TEXT_SELECTOR: string = 'text-cell-component';
|
||||
|
||||
@@ -25,6 +30,7 @@ export const TEXT_SELECTOR: string = 'text-cell-component';
|
||||
})
|
||||
export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
|
||||
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
|
||||
@Input() cellModel: ICellModel;
|
||||
|
||||
@Input() set model(value: NotebookModel) {
|
||||
@@ -38,18 +44,25 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
private _content: string;
|
||||
private isEditMode: boolean;
|
||||
private _sanitizer: ISanitizer;
|
||||
private _previewCssApplied: boolean = false;
|
||||
private _model: NotebookModel;
|
||||
private _activeCellId: string;
|
||||
private readonly _onDidClickLink = this._register(new Emitter<URI>());
|
||||
public readonly onDidClickLink = this._onDidClickLink.event;
|
||||
protected isLoading: boolean;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(ICommandService) private _commandService: ICommandService
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@Inject(IOpenerService) private readonly openerService: IOpenerService,
|
||||
) {
|
||||
super();
|
||||
this.isEditMode = false;
|
||||
this.isLoading = true;
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
}
|
||||
|
||||
//Gets sanitizer from ISanitizer interface
|
||||
@@ -68,8 +81,14 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
return this._activeCellId;
|
||||
}
|
||||
|
||||
private setLoading(isLoading: boolean): void {
|
||||
this.isLoading = isLoading;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.updatePreview();
|
||||
this.setLoading(false);
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
this.cellModel.onOutputsChanged(e => {
|
||||
@@ -124,32 +143,24 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
|
||||
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
||||
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
}
|
||||
|
||||
public handleContentChanged(): void {
|
||||
if (!this._previewCssApplied) {
|
||||
this.updatePreviewCssClass();
|
||||
}
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
public toggleEditMode(editMode?: boolean): void {
|
||||
this.isEditMode = editMode !== undefined? editMode : !this.isEditMode;
|
||||
this.updatePreviewCssClass();
|
||||
if (!this.isEditMode && this.cellModel.id === this._activeCellId) {
|
||||
this._cellToggleMoreActions.toggle(true, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
else {
|
||||
this._cellToggleMoreActions.toggle(false, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
this.updatePreview();
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
// Updates the css class to preview 'div' based on edit mode
|
||||
private updatePreviewCssClass() {
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
if (this.isEditMode && this.cellModel.source) {
|
||||
outputElement.className = 'notebook-preview';
|
||||
this._previewCssApplied = true;
|
||||
}
|
||||
else {
|
||||
outputElement.className = '';
|
||||
this._previewCssApplied = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,6 @@ text-cell-component .notebook-preview {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
user-select: initial;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
@@ -38,15 +38,15 @@ export class CellModel implements ICellModel {
|
||||
constructor(private factory: IModelFactory, cellData?: nb.ICellContents, private _options?: ICellModelOptions) {
|
||||
this.id = `${modelId++}`;
|
||||
CellModel.CreateLanguageMappings();
|
||||
// Do nothing for now
|
||||
if (cellData) {
|
||||
// Read in contents if available
|
||||
this.fromJSON(cellData);
|
||||
} else {
|
||||
this._cellType = CellTypes.Code;
|
||||
this._source = '';
|
||||
}
|
||||
this._isEditMode = this._cellType !== CellTypes.Markdown;
|
||||
this.setDefaultLanguage();
|
||||
this.ensureDefaultLanguage();
|
||||
if (_options && _options.isTrusted) {
|
||||
this._isTrusted = true;
|
||||
} else {
|
||||
@@ -284,7 +284,7 @@ export class CellModel implements ICellModel {
|
||||
}
|
||||
this._cellType = cell.cell_type;
|
||||
this._source = Array.isArray(cell.source) ? cell.source.join('') : cell.source;
|
||||
this._language = (cell.metadata && cell.metadata.language) ? cell.metadata.language : 'python';
|
||||
this.setLanguageFromContents(cell);
|
||||
if (cell.outputs) {
|
||||
for (let output of cell.outputs) {
|
||||
// For now, we're assuming it's OK to save these as-is with no modification
|
||||
@@ -293,6 +293,15 @@ export class CellModel implements ICellModel {
|
||||
}
|
||||
}
|
||||
|
||||
private setLanguageFromContents(cell: nb.ICellContents): void {
|
||||
if (cell.cell_type === CellTypes.Markdown) {
|
||||
this._language = 'markdown';
|
||||
} else if (cell.metadata && cell.metadata.language) {
|
||||
this._language = cell.metadata.language;
|
||||
}
|
||||
// else skip, we set default language anyhow
|
||||
}
|
||||
|
||||
private addOutput(output: nb.ICellOutput) {
|
||||
this._normalize(output);
|
||||
this._outputs.push(output);
|
||||
@@ -327,8 +336,32 @@ export class CellModel implements ICellModel {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private setDefaultLanguage(): void {
|
||||
this._language = 'python';
|
||||
/**
|
||||
* Ensures there is a default language set, if none was already defined.
|
||||
* Will read information from the overall Notebook (passed as options to the model), or
|
||||
* if all else fails default back to python.
|
||||
*
|
||||
*/
|
||||
private ensureDefaultLanguage(): void {
|
||||
// See if language is already set / is known based on cell type
|
||||
if (this.hasLanguage()) {
|
||||
return;
|
||||
}
|
||||
if (this._cellType === CellTypes.Markdown) {
|
||||
this._language = 'markdown';
|
||||
return;
|
||||
}
|
||||
|
||||
// try set it based on overall Notebook language
|
||||
this.trySetLanguageFromLangInfo();
|
||||
|
||||
// fallback to python
|
||||
if (!this._language) {
|
||||
this._language = 'python';
|
||||
}
|
||||
}
|
||||
|
||||
private trySetLanguageFromLangInfo() {
|
||||
// In languageInfo, set the language to the "name" property
|
||||
// If the "name" property isn't defined, check the "mimeType" property
|
||||
// Otherwise, default to python as the language
|
||||
@@ -338,16 +371,25 @@ export class CellModel implements ICellModel {
|
||||
// check the LanguageMapping to determine if a mapping is necessary (example 'pyspark' -> 'python')
|
||||
if (CellModel.LanguageMapping[languageInfo.name]) {
|
||||
this._language = CellModel.LanguageMapping[languageInfo.name];
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this._language = languageInfo.name;
|
||||
}
|
||||
} else if (languageInfo.mimetype) {
|
||||
}
|
||||
else if (languageInfo.mimetype) {
|
||||
this._language = languageInfo.mimetype;
|
||||
}
|
||||
}
|
||||
let mimeTypePrefix = 'x-';
|
||||
if (this._language.includes(mimeTypePrefix)) {
|
||||
this._language = this._language.replace(mimeTypePrefix, '');
|
||||
|
||||
if (this._language) {
|
||||
let mimeTypePrefix = 'x-';
|
||||
if (this._language.includes(mimeTypePrefix)) {
|
||||
this._language = this._language.replace(mimeTypePrefix, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hasLanguage(): boolean {
|
||||
return !!this._language;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +288,12 @@ export interface INotebookModel {
|
||||
*/
|
||||
readonly contexts: IDefaultConnection | undefined;
|
||||
|
||||
/**
|
||||
* Event fired on first initialization of the cells and
|
||||
* on subsequent change events
|
||||
*/
|
||||
readonly contentChanged: Event<NotebookContentChange>;
|
||||
|
||||
/**
|
||||
* The trusted mode of the Notebook
|
||||
*/
|
||||
@@ -339,6 +345,29 @@ export interface INotebookModel {
|
||||
pushEditOperations(edits: ISingleNotebookEditOperation[]): void;
|
||||
}
|
||||
|
||||
export interface NotebookContentChange {
|
||||
/**
|
||||
* The type of change that occurred
|
||||
*/
|
||||
changeType: NotebookChangeType;
|
||||
/**
|
||||
* Optional cells that were changed
|
||||
*/
|
||||
cells?: ICellModel | ICellModel[];
|
||||
/**
|
||||
* Optional index of the change, indicating the cell at which an insert or
|
||||
* delete occurred
|
||||
*/
|
||||
cellIndex?: number;
|
||||
/**
|
||||
* Optional value indicating if the notebook is in a dirty or clean state after this change
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof NotebookContentChange
|
||||
*/
|
||||
isDirty?: boolean;
|
||||
}
|
||||
|
||||
export interface ICellModelOptions {
|
||||
notebook: INotebookModel;
|
||||
isTrusted: boolean;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
import { CellModel } from './cell';
|
||||
import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, notebookConstants } from './modelInterfaces';
|
||||
import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, notebookConstants, NotebookContentChange } from './modelInterfaces';
|
||||
import { NotebookChangeType, CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { nbversion } from '../notebookConstants';
|
||||
import * as notebookUtils from '../notebookUtils';
|
||||
@@ -22,6 +22,7 @@ import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
|
||||
import { INotification, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
|
||||
/*
|
||||
@@ -38,28 +39,6 @@ export class ErrorInfo {
|
||||
constructor(public readonly message: string, public readonly severity: MessageLevel) {
|
||||
}
|
||||
}
|
||||
export interface NotebookContentChange {
|
||||
/**
|
||||
* What was the change that occurred?
|
||||
*/
|
||||
changeType: NotebookChangeType;
|
||||
/**
|
||||
* Optional cells that were changed
|
||||
*/
|
||||
cells?: ICellModel | ICellModel[];
|
||||
/**
|
||||
* Optional index of the change, indicating the cell at which an insert or
|
||||
* delete occurred
|
||||
*/
|
||||
cellIndex?: number;
|
||||
/**
|
||||
* Optional value indicating if the notebook is in a dirty or clean state after this change
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof NotebookContentChange
|
||||
*/
|
||||
isDirty?: boolean;
|
||||
}
|
||||
|
||||
export class NotebookModel extends Disposable implements INotebookModel {
|
||||
private _contextsChangedEmitter = new Emitter<void>();
|
||||
@@ -97,6 +76,13 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
return this.notebookOptions.notebookManager;
|
||||
}
|
||||
|
||||
public get notebookUri() : URI {
|
||||
return this.notebookOptions.notebookUri;
|
||||
}
|
||||
public set notebookUri(value : URI) {
|
||||
this.notebookOptions.notebookUri = value;
|
||||
}
|
||||
|
||||
public get hasServerManager(): boolean {
|
||||
// If the service has a server manager, then we can show the start button
|
||||
return !!this.notebookManager.serverManager;
|
||||
@@ -364,7 +350,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
if (!newConnection && this._activeContexts.defaultConnection.options['host'] === host) {
|
||||
newConnection = this._activeContexts.defaultConnection;
|
||||
}
|
||||
SparkMagicContexts.configureContext(this.notebookOptions);
|
||||
SparkMagicContexts.configureContext();
|
||||
this._hadoopConnection = new NotebookConnection(newConnection);
|
||||
this.refreshConnections(newConnection);
|
||||
this._clientSession.updateConnection(this._hadoopConnection);
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import * as path from 'path';
|
||||
import { nb } from 'sqlops';
|
||||
|
||||
import * as json from 'vs/base/common/json';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
@@ -17,11 +16,48 @@ import * as notebookUtils from '../notebookUtils';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||
|
||||
const configBase = {
|
||||
'kernel_python_credentials': {
|
||||
'url': ''
|
||||
},
|
||||
'kernel_scala_credentials': {
|
||||
'url': ''
|
||||
},
|
||||
'kernel_r_credentials': {
|
||||
'url': ''
|
||||
},
|
||||
|
||||
'ignore_ssl_errors': true,
|
||||
|
||||
'logging_config': {
|
||||
'version': 1,
|
||||
'formatters': {
|
||||
'magicsFormatter': {
|
||||
'format': '%(asctime)s\t%(levelname)s\t%(message)s',
|
||||
'datefmt': ''
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'magicsHandler': {
|
||||
'class': 'hdijupyterutils.filehandler.MagicsFileHandler',
|
||||
'formatter': 'magicsFormatter',
|
||||
'home_path': ''
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'magicsLogger': {
|
||||
'handlers': ['magicsHandler'],
|
||||
'level': 'DEBUG',
|
||||
'propagate': 0
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export class SparkMagicContexts {
|
||||
|
||||
public static get DefaultContext(): IDefaultConnection {
|
||||
// TODO NOTEBOOK REFACTOR fix default connection handling
|
||||
let defaultConnection: IConnectionProfile = <any> {
|
||||
let defaultConnection: IConnectionProfile = <any>{
|
||||
providerName: notebookConstants.hadoopKnoxProviderName,
|
||||
id: '-1',
|
||||
options:
|
||||
@@ -47,7 +83,7 @@ export class SparkMagicContexts {
|
||||
let connections: IDefaultConnection = this.DefaultContext;
|
||||
if (!profile) {
|
||||
if (!kernelChangedArgs || !kernelChangedArgs.newValue ||
|
||||
(kernelChangedArgs.oldValue && kernelChangedArgs.newValue.id === kernelChangedArgs.oldValue.id)) {
|
||||
(kernelChangedArgs.oldValue && kernelChangedArgs.newValue.id === kernelChangedArgs.oldValue.id)) {
|
||||
// nothing to do, kernels are the same or new kernel is undefined
|
||||
return connections;
|
||||
}
|
||||
@@ -55,7 +91,7 @@ export class SparkMagicContexts {
|
||||
if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name) {
|
||||
switch (kernelChangedArgs.newValue.name) {
|
||||
case (notebookConstants.python3):
|
||||
// python3 case, use this.DefaultContext for the only connection
|
||||
// python3 case, use this.DefaultContext for the only connection
|
||||
break;
|
||||
//TO DO: Handle server connections based on kernel type. Right now, we call the same method for all kernel types.
|
||||
default:
|
||||
@@ -76,13 +112,13 @@ export class SparkMagicContexts {
|
||||
let defaultConnection: IConnectionProfile = SparkMagicContexts.DefaultContext.defaultConnection;
|
||||
let activeConnections: IConnectionProfile[] = await connectionService.getActiveConnections();
|
||||
// If no connections exist, only show 'n/a'
|
||||
if (activeConnections && activeConnections.length > 0) {
|
||||
// Remove all non-Spark connections
|
||||
activeConnections = activeConnections.filter(conn => conn.providerName === notebookConstants.hadoopKnoxProviderName);
|
||||
}
|
||||
if (activeConnections.length === 0) {
|
||||
return SparkMagicContexts.DefaultContext;
|
||||
}
|
||||
if (activeConnections && activeConnections.length > 0) {
|
||||
// Remove all non-Spark connections
|
||||
activeConnections = activeConnections.filter(conn => conn.providerName === notebookConstants.hadoopKnoxProviderName);
|
||||
}
|
||||
if (activeConnections.length === 0) {
|
||||
return SparkMagicContexts.DefaultContext;
|
||||
}
|
||||
|
||||
// If launched from the right click or server dashboard, connection profile data exists, so use that as default
|
||||
if (profile && profile.options) {
|
||||
@@ -95,7 +131,7 @@ export class SparkMagicContexts {
|
||||
defaultConnection = activeConnections[0];
|
||||
} else {
|
||||
// TODO NOTEBOOK REFACTOR change this so it's no longer incompatible with IConnectionProfile
|
||||
defaultConnection = <IConnectionProfile> <any>{
|
||||
defaultConnection = <IConnectionProfile><any>{
|
||||
providerName: notebookConstants.hadoopKnoxProviderName,
|
||||
id: '-1',
|
||||
options:
|
||||
@@ -107,31 +143,28 @@ export class SparkMagicContexts {
|
||||
}
|
||||
}
|
||||
return {
|
||||
otherConnections: activeConnections,
|
||||
defaultConnection: defaultConnection
|
||||
otherConnections: activeConnections,
|
||||
defaultConnection: defaultConnection
|
||||
};
|
||||
}
|
||||
|
||||
public static async configureContext(options: INotebookModelOptions): Promise<object> {
|
||||
public static async configureContext(): Promise<object> {
|
||||
let sparkmagicConfDir = path.join(notebookUtils.getUserHome(), '.sparkmagic');
|
||||
// TODO NOTEBOOK REFACTOR re-enable this or move to extension. Requires config files to be available in order to work
|
||||
// await notebookUtils.mkDir(sparkmagicConfDir);
|
||||
await notebookUtils.mkDir(sparkmagicConfDir);
|
||||
|
||||
// // Default to localhost in config file.
|
||||
// let creds: ICredentials = {
|
||||
// 'url': 'http://localhost:8088'
|
||||
// };
|
||||
// Default to localhost in config file.
|
||||
let creds: ICredentials = {
|
||||
'url': 'http://localhost:8088'
|
||||
};
|
||||
|
||||
// let configPath = notebookUtils.getTemplatePath(options.extensionContext.extensionPath, path.join('jupyter_config', 'sparkmagic_config.json'));
|
||||
// let fileBuffer: Buffer = await pfs.readFile(configPath);
|
||||
// let fileContents: string = fileBuffer.toString();
|
||||
// let config: ISparkMagicConfig = json.parse(fileContents);
|
||||
// SparkMagicContexts.updateConfig(config, creds, sparkmagicConfDir);
|
||||
let config: ISparkMagicConfig = Object.assign({}, configBase);
|
||||
SparkMagicContexts.updateConfig(config, creds, sparkmagicConfDir);
|
||||
|
||||
// let configFilePath = path.join(sparkmagicConfDir, 'config.json');
|
||||
// await pfs.writeFile(configFilePath, JSON.stringify(config));
|
||||
let configFilePath = path.join(sparkmagicConfDir, 'config.json');
|
||||
await pfs.writeFile(configFilePath, JSON.stringify(config));
|
||||
|
||||
return {'SPARKMAGIC_CONF_DIR': sparkmagicConfDir};
|
||||
return { 'SPARKMAGIC_CONF_DIR': sparkmagicConfDir };
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -149,10 +182,10 @@ export class SparkMagicContexts {
|
||||
}
|
||||
}
|
||||
let profile = connectionInfo as IConnectionProfile;
|
||||
if (foundSavedKernelInSpecs && specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) {
|
||||
if (specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) {
|
||||
// set default kernel to default spark kernel if profile exists
|
||||
// otherwise, set default to kernel info loaded from existing file
|
||||
defaultKernel = !savedKernelInfo ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : savedKernelInfo;
|
||||
defaultKernel = !foundSavedKernelInSpecs ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : foundSavedKernelInSpecs;
|
||||
} else {
|
||||
// Handle kernels
|
||||
if (savedKernelInfo && savedKernelInfo.name.toLowerCase().indexOf('spark') > -1) {
|
||||
@@ -186,10 +219,13 @@ interface ISparkMagicConfig {
|
||||
kernel_python_credentials: ICredentials;
|
||||
kernel_scala_credentials: ICredentials;
|
||||
kernel_r_credentials: ICredentials;
|
||||
ignore_ssl_errors?: boolean;
|
||||
logging_config: {
|
||||
handlers: {
|
||||
magicsHandler: {
|
||||
home_path: string;
|
||||
class?: string;
|
||||
formatter?: string
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div #toolbar class="editor-toolbar actionbar-container" style="flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center; height: 36px">
|
||||
<div #toolbar class="editor-toolbar actionbar-container" style="flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center;">
|
||||
</div>
|
||||
<div class="scrollable" style="flex: 1 1 auto; position: relative" (click)="unselectActiveCell()">
|
||||
<loading-spinner [loading]="isLoading"></loading-spinner>
|
||||
|
||||
@@ -5,42 +5,50 @@
|
||||
|
||||
import './notebookStyles';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
|
||||
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnDestroy } from '@angular/core';
|
||||
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { INotificationService, INotification } from 'vs/platform/notification/common/notification';
|
||||
import { INotificationService, INotification, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
||||
|
||||
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { ICellModel, IModelFactory, notebookConstants } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor } from 'sql/services/notebook/notebookService';
|
||||
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { NotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
||||
import * as notebookUtils from './notebookUtils';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, SaveNotebookAction } from 'sql/parts/notebook/notebookActions';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IAction, Action, IActionItem } from 'vs/base/common/actions';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { fillInActions, LabeledMenuItemActionItem } from 'vs/platform/actions/browser/menuItemActionItem';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IHistoryService } from 'vs/workbench/services/history/common/history';
|
||||
import * as paths from 'vs/base/common/paths';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { VIEWLET_ID, IExtensionsViewlet } from 'vs/workbench/parts/extensions/common/extensions';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
||||
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { ICellModel, IModelFactory, notebookConstants, INotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_FILETYPE, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
|
||||
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
||||
import * as notebookUtils from 'sql/parts/notebook/notebookUtils';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, SaveNotebookAction } from 'sql/parts/notebook/notebookActions';
|
||||
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { IResourceInput } from 'vs/platform/editor/common/editor';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
|
||||
export const NOTEBOOK_SELECTOR: string = 'notebook-component';
|
||||
|
||||
@@ -81,7 +89,12 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
@Inject(IConnectionDialogService) private connectionDialogService: IConnectionDialogService,
|
||||
@Inject(IContextKeyService) private contextKeyService: IContextKeyService,
|
||||
@Inject(IMenuService) private menuService: IMenuService,
|
||||
@Inject(IKeybindingService) private keybindingService: IKeybindingService
|
||||
@Inject(IKeybindingService) private keybindingService: IKeybindingService,
|
||||
@Inject(IHistoryService) private historyService: IHistoryService,
|
||||
@Inject(IWindowService) private windowService: IWindowService,
|
||||
@Inject(IViewletService) private viewletService: IViewletService,
|
||||
@Inject(IUntitledEditorService) private untitledEditorService: IUntitledEditorService,
|
||||
@Inject(IEditorGroupsService) private editorGroupService: IEditorGroupsService
|
||||
) {
|
||||
super();
|
||||
this.updateProfile();
|
||||
@@ -132,7 +145,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
return this._modelRegisteredDeferred.promise;
|
||||
}
|
||||
|
||||
protected get cells(): ReadonlyArray<ICellModel> {
|
||||
public get cells(): ICellModel[] {
|
||||
return this._model ? this._model.cells : [];
|
||||
}
|
||||
|
||||
@@ -222,6 +235,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
}
|
||||
|
||||
private async loadModel(): Promise<void> {
|
||||
await this.awaitNonDefaultProvider();
|
||||
this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this._notebookParams.providerId, this._notebookParams.notebookUri);
|
||||
let model = new NotebookModel({
|
||||
factory: this.modelFactory,
|
||||
@@ -241,6 +255,34 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private async awaitNonDefaultProvider(): Promise<void> {
|
||||
// Wait on registration for now. Long-term would be good to cache and refresh
|
||||
await this.notebookService.registrationComplete;
|
||||
// Refresh the provider if we had been using default
|
||||
if (DEFAULT_NOTEBOOK_PROVIDER === this._notebookParams.providerId) {
|
||||
this._notebookParams.providerId = notebookUtils.getProviderForFileName(this._notebookParams.notebookUri.fsPath, this.notebookService);
|
||||
}
|
||||
if (DEFAULT_NOTEBOOK_PROVIDER === this._notebookParams.providerId) {
|
||||
// If it's still the default, warn them they should install an extension
|
||||
this.notificationService.prompt(Severity.Warning,
|
||||
localize('noKernelInstalled', 'Please install the SQL Server 2019 extension to run cells'),
|
||||
[{
|
||||
label: localize('installSql2019Extension', 'Install Extension'),
|
||||
run: () => this.openExtensionGallery()
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
private async openExtensionGallery(): Promise<void> {
|
||||
try {
|
||||
let viewlet = await this.viewletService.openViewlet(VIEWLET_ID, true) as IExtensionsViewlet;
|
||||
viewlet.search('sql-vnext');
|
||||
viewlet.focus();
|
||||
} catch (error) {
|
||||
this.notificationService.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Updates toolbar components
|
||||
private updateToolbarComponents(isTrusted: boolean)
|
||||
{
|
||||
@@ -332,7 +374,78 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
}
|
||||
}
|
||||
|
||||
// Gets file path from recent workspace in local
|
||||
private getLastActiveFilePath(untitledResource: URI): string {
|
||||
let fileName = untitledResource.path + '.' + DEFAULT_NOTEBOOK_FILETYPE.toLocaleLowerCase();
|
||||
|
||||
let lastActiveFile = this.historyService.getLastActiveFile();
|
||||
if (lastActiveFile) {
|
||||
return URI.file(paths.join(paths.dirname(lastActiveFile.fsPath), fileName)).fsPath;
|
||||
}
|
||||
|
||||
let lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot('file');
|
||||
if (lastActiveFolder) {
|
||||
return URI.file(paths.join(lastActiveFolder.fsPath, fileName)).fsPath;
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
promptForPath(defaultPath: string): TPromise<string> {
|
||||
return this.windowService.showSaveDialog({
|
||||
defaultPath: defaultPath,
|
||||
filters: [{ name: localize('notebookFile', 'Notebook'), extensions: ['ipynb']}]
|
||||
});
|
||||
}
|
||||
|
||||
// Entry point to save notebook
|
||||
public async save(): Promise<boolean> {
|
||||
let self = this;
|
||||
let notebookUri = this.notebookParams.notebookUri;
|
||||
if (notebookUri.scheme === Schemas.untitled) {
|
||||
let dialogPath = this.getLastActiveFilePath(notebookUri);
|
||||
return this.promptForPath(dialogPath).then(path => {
|
||||
if (path) {
|
||||
let target = URI.file(path);
|
||||
let resource = self._model.notebookUri;
|
||||
self._model.notebookUri = target;
|
||||
this.saveNotebook().then(result => {
|
||||
if(result)
|
||||
{
|
||||
return this.replaceUntitledNotebookEditor(resource, target);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
return false; // User clicks cancel
|
||||
});
|
||||
}
|
||||
else {
|
||||
return await this.saveNotebook();
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces untitled notebook editor with the saved file name
|
||||
private async replaceUntitledNotebookEditor(resource: URI, target: URI): Promise<boolean> {
|
||||
let encodingOfSource = this.untitledEditorService.getEncoding(resource);
|
||||
const replacement: IResourceInput = {
|
||||
resource: target,
|
||||
encoding: encodingOfSource,
|
||||
options: {
|
||||
pinned: true
|
||||
}
|
||||
};
|
||||
|
||||
return TPromise.join(this.editorGroupService.groups.map(g =>
|
||||
this.editorService.replaceEditors([{
|
||||
editor: { resource },
|
||||
replacement
|
||||
}], g))).then(() => {
|
||||
this.notebookService.renameNotebookEditor(resource, target, this);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private async saveNotebook(): Promise<boolean> {
|
||||
try {
|
||||
let saved = await this._model.saveModel();
|
||||
if (saved) {
|
||||
@@ -368,6 +481,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
return this._notebookParams.notebookUri.toString();
|
||||
}
|
||||
|
||||
public get modelReady(): Promise<INotebookModel> {
|
||||
return this._modelReadyDeferred.promise;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.editorService.activeEditor === this.notebookParams.input;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
.notebookEditor .monaco-select-box {
|
||||
min-width: 150px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button.icon-add{
|
||||
@@ -74,4 +74,9 @@
|
||||
.vs-dark .notebookEditor .notebook-button.icon-save,
|
||||
.hc-black .notebookEditor .notebook-button.icon-save{
|
||||
background-image: url("./media/dark/save_inverse.svg");
|
||||
}
|
||||
|
||||
.moreActions .action-label.icon.toggle-more {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
@@ -26,7 +26,6 @@ const attachToLabel: string = localize('AttachTo', 'Attach to: ');
|
||||
const msgLoadingContexts = localize('loadingContexts', 'Loading contexts...');
|
||||
const msgAddNewConnection = localize('addNewConnection', 'Add new connection');
|
||||
const msgSelectConnection = localize('selectConnection', 'Select connection');
|
||||
const msgConnectionNotApplicable = localize('connectionNotSupported', 'n/a');
|
||||
const msgLocalHost = localize('localhost', 'Localhost');
|
||||
|
||||
// Action to add a cell to notebook based on cell type(code/markdown).
|
||||
@@ -65,8 +64,6 @@ export class SaveNotebookAction extends Action {
|
||||
let saved = await context.save();
|
||||
if (saved) {
|
||||
this._notificationService.notify({ severity: Severity.Info, message: SaveNotebookAction.notebookSavedMsg, actions });
|
||||
} else {
|
||||
this._notificationService.error(SaveNotebookAction.notebookFailedSaveMsg);
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
@@ -177,4 +177,20 @@ export class NotebookInput extends EditorInput {
|
||||
setDirty(isDirty: boolean): void {
|
||||
this._model.setDirty(isDirty);
|
||||
}
|
||||
|
||||
|
||||
public matches(otherInput: any): boolean {
|
||||
if (super.matches(otherInput) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (otherInput instanceof NotebookInput) {
|
||||
const otherNotebookEditorInput = <NotebookInput>otherInput;
|
||||
|
||||
// Compare by resource
|
||||
return otherNotebookEditorInput.notebookUri.toString() === this.notebookUri.toString();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,7 @@ import * as os from 'os';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IOutputChannel } from 'vs/workbench/parts/output/common/output';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { INotebookProviderRegistry, Extensions } from 'sql/services/notebook/notebookRegistry';
|
||||
import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE } from 'sql/services/notebook/notebookService';
|
||||
import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService } from 'sql/services/notebook/notebookService';
|
||||
|
||||
|
||||
/**
|
||||
@@ -41,18 +39,17 @@ export async function mkDir(dirPath: string, outputChannel?: IOutputChannel): Pr
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderForFileName(fileName: string): string {
|
||||
export function getProviderForFileName(fileName: string, notebookService: INotebookService): string {
|
||||
let fileExt = path.extname(fileName);
|
||||
let provider: string;
|
||||
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
||||
// First try to get provider for actual file type
|
||||
if (fileExt && fileExt.startsWith('.')) {
|
||||
fileExt = fileExt.slice(1,fileExt.length);
|
||||
provider = notebookRegistry.getProviderForFileType(fileExt);
|
||||
provider = notebookService.getProviderForFileType(fileExt);
|
||||
}
|
||||
// Fallback to provider for default file type (assume this is a global handler)
|
||||
if (!provider) {
|
||||
provider = notebookRegistry.getProviderForFileType(DEFAULT_NOTEBOOK_FILETYPE);
|
||||
provider = notebookService.getProviderForFileType(DEFAULT_NOTEBOOK_FILETYPE);
|
||||
}
|
||||
// Finally if all else fails, use the built-in handler
|
||||
if (!provider) {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Sash, IHorizontalSashLayoutProvider, ISashEvent, Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
|
||||
import { FIND_IDS, MATCHES_LIMIT, CONTEXT_FIND_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel';
|
||||
import { FIND_IDS, CONTEXT_FIND_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel';
|
||||
import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ITheme, registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
@@ -36,7 +36,7 @@ const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find");
|
||||
const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match");
|
||||
const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match");
|
||||
const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close");
|
||||
const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Only the first 999 results are highlighted, but all find operations work on the entire text.");
|
||||
const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Your search returned a large number of results, only the first 999 matches will be highlighted.");
|
||||
const NLS_MATCHES_LOCATION = nls.localize('label.matchesLocation', "{0} of {1}");
|
||||
const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results");
|
||||
|
||||
@@ -46,6 +46,8 @@ const FIND_INPUT_AREA_WIDTH = PART_WIDTH - 54;
|
||||
|
||||
let MAX_MATCHES_COUNT_WIDTH = 69;
|
||||
|
||||
export const PROFILER_MAX_MATCHES = 999;
|
||||
|
||||
export const ACTION_IDS = {
|
||||
FIND_NEXT: 'findNext',
|
||||
FIND_PREVIOUS: 'findPrev'
|
||||
@@ -86,6 +88,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
|
||||
|
||||
private _resizeSash: Sash;
|
||||
|
||||
private searchTimeoutHandle: number;
|
||||
|
||||
constructor(
|
||||
tableController: ITableController,
|
||||
state: FindReplaceState,
|
||||
@@ -213,7 +217,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
|
||||
|
||||
private _updateMatchesCount(): void {
|
||||
this._matchesCount.style.minWidth = MAX_MATCHES_COUNT_WIDTH + 'px';
|
||||
if (this._state.matchesCount >= MATCHES_LIMIT) {
|
||||
if (this._state.matchesCount >= PROFILER_MAX_MATCHES) {
|
||||
this._matchesCount.title = NLS_MATCHES_COUNT_LIMIT_TITLE;
|
||||
} else {
|
||||
this._matchesCount.title = '';
|
||||
@@ -227,8 +231,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
|
||||
let label: string;
|
||||
if (this._state.matchesCount > 0) {
|
||||
let matchesCount: string = String(this._state.matchesCount);
|
||||
if (this._state.matchesCount >= MATCHES_LIMIT) {
|
||||
matchesCount += '+';
|
||||
if (this._state.matchesCount >= PROFILER_MAX_MATCHES) {
|
||||
matchesCount = PROFILER_MAX_MATCHES + '+';
|
||||
}
|
||||
let matchesPosition: string = String(this._state.matchesPosition);
|
||||
if (matchesPosition === '0') {
|
||||
@@ -401,7 +405,14 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
|
||||
this._findInput.setWholeWords(!!this._state.wholeWord);
|
||||
this._register(this._findInput.onKeyDown((e) => this._onFindInputKeyDown(e)));
|
||||
this._register(this._findInput.onInput(() => {
|
||||
this._state.change({ searchString: this._findInput.getValue() }, true);
|
||||
let self = this;
|
||||
if (self.searchTimeoutHandle) {
|
||||
clearTimeout(self.searchTimeoutHandle);
|
||||
}
|
||||
|
||||
this.searchTimeoutHandle = setTimeout(function () {
|
||||
self._state.change({ searchString: self._findInput.getValue() }, true);
|
||||
}, 300);
|
||||
}));
|
||||
this._register(this._findInput.onDidOptionChange(() => {
|
||||
this._state.change({
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Dimension } from 'vs/base/browser/dom';
|
||||
import { textFormatter } from 'sql/parts/grid/services/sharedServices';
|
||||
import { PROFILER_MAX_MATCHES } from 'sql/parts/profiler/editor/controller/profilerFindWidget';
|
||||
|
||||
export interface ProfilerTableViewState {
|
||||
scrollTop: number;
|
||||
@@ -214,7 +215,7 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll
|
||||
if (e.searchString) {
|
||||
if (this._input && this._input.data) {
|
||||
if (this._findState.searchString) {
|
||||
this._input.data.find(this._findState.searchString).then(p => {
|
||||
this._input.data.find(this._findState.searchString, PROFILER_MAX_MATCHES).then(p => {
|
||||
if (p) {
|
||||
this._profilerTable.setActiveCell(p.row, p.col);
|
||||
this._updateFinderMatchState();
|
||||
|
||||
@@ -305,6 +305,11 @@ let registryProperties = {
|
||||
'description': localize('sql.saveAsCsv.encoding', '[Optional] File encoding used when saving results as CSV'),
|
||||
'default': 'utf-8'
|
||||
},
|
||||
'sql.results.streaming': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.results.streaming', 'Enable results streaming; contains few minor visual issues'),
|
||||
'default': false
|
||||
},
|
||||
'sql.copyIncludeHeaders': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.copyIncludeHeaders', '[Optional] Configuration options for copying results from the Results View'),
|
||||
|
||||
@@ -88,7 +88,8 @@ export class GridTableState extends Disposable {
|
||||
private _canBeMaximized: boolean;
|
||||
|
||||
/* The top row of the current scroll */
|
||||
public scrollPosition = 0;
|
||||
public scrollPositionY = 0;
|
||||
public scrollPositionX = 0;
|
||||
public selection: Slick.Range[];
|
||||
public activeCell: Slick.Cell;
|
||||
|
||||
@@ -145,7 +146,7 @@ export class GridPanel extends ViewletPanel {
|
||||
super(options, keybindingService, contextMenuService, configurationService);
|
||||
this.splitView = new ScrollableSplitView(this.container, { enableResizing: false, verticalScrollbarVisibility: ScrollbarVisibility.Visible });
|
||||
this.splitView.onScroll(e => {
|
||||
if (this.state) {
|
||||
if (this.state && this.splitView.length !== 0) {
|
||||
this.state.scrollPosition = e;
|
||||
}
|
||||
});
|
||||
@@ -185,62 +186,97 @@ export class GridPanel extends ViewletPanel {
|
||||
}
|
||||
this.reset();
|
||||
}));
|
||||
this.addResultSet(this.runner.batchSets.reduce<sqlops.ResultSetSummary[]>((p, e) => {
|
||||
if (this.configurationService.getValue<boolean>('sql.results.streaming')) {
|
||||
p = p.concat(e.resultSetSummaries);
|
||||
} else {
|
||||
p = p.concat(e.resultSetSummaries.filter(c => c.complete));
|
||||
}
|
||||
return p;
|
||||
}, []));
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private onResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
this.addResultSet(resultSet);
|
||||
|
||||
this.tables.map(t => {
|
||||
t.state.canBeMaximized = this.tables.length > 1;
|
||||
});
|
||||
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private updateResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
|
||||
let resultsToUpdate: sqlops.ResultSetSummary[];
|
||||
if (!Array.isArray(resultSet)) {
|
||||
resultsToUpdate = [resultSet];
|
||||
} else {
|
||||
resultsToUpdate = resultSet;
|
||||
}
|
||||
|
||||
for (let set of resultsToUpdate) {
|
||||
let table = this.tables.find(t => t.resultSet.batchId === set.batchId && t.resultSet.id === set.id);
|
||||
if (table) {
|
||||
table.updateResult(set);
|
||||
} else {
|
||||
warn('Got result set update request for non-existant table');
|
||||
}
|
||||
}
|
||||
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private addResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
let resultsToAdd: sqlops.ResultSetSummary[];
|
||||
if (!Array.isArray(resultSet)) {
|
||||
resultsToAdd = [resultSet];
|
||||
} else {
|
||||
resultsToAdd = resultSet;
|
||||
resultsToAdd = resultSet.splice(0);
|
||||
}
|
||||
const sizeChanges = () => {
|
||||
this.tables.map(t => {
|
||||
t.state.canBeMaximized = this.tables.length > 1;
|
||||
});
|
||||
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.configurationService.getValue<boolean>('sql.results.streaming')) {
|
||||
this.addResultSet(resultsToAdd);
|
||||
sizeChanges();
|
||||
} else {
|
||||
resultsToAdd = resultsToAdd.filter(e => e.complete);
|
||||
if (resultsToAdd.length > 0) {
|
||||
this.addResultSet(resultsToAdd);
|
||||
}
|
||||
sizeChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private updateResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
let resultsToUpdate: sqlops.ResultSetSummary[];
|
||||
if (!Array.isArray(resultSet)) {
|
||||
resultsToUpdate = [resultSet];
|
||||
} else {
|
||||
resultsToUpdate = resultSet.splice(0);
|
||||
}
|
||||
|
||||
const sizeChanges = () => {
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.configurationService.getValue<boolean>('sql.results.streaming')) {
|
||||
for (let set of resultsToUpdate) {
|
||||
let table = this.tables.find(t => t.resultSet.batchId === set.batchId && t.resultSet.id === set.id);
|
||||
if (table) {
|
||||
table.updateResult(set);
|
||||
} else {
|
||||
warn('Got result set update request for non-existant table');
|
||||
}
|
||||
}
|
||||
sizeChanges();
|
||||
} else {
|
||||
resultsToUpdate = resultsToUpdate.filter(e => e.complete);
|
||||
if (resultsToUpdate.length > 0) {
|
||||
this.addResultSet(resultsToUpdate);
|
||||
}
|
||||
sizeChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private addResultSet(resultSet: sqlops.ResultSetSummary[]) {
|
||||
let tables: GridTable<any>[] = [];
|
||||
|
||||
for (let set of resultsToAdd) {
|
||||
for (let set of resultSet) {
|
||||
let tableState: GridTableState;
|
||||
if (this._state) {
|
||||
tableState = this.state.tableStates.find(e => e.batchId === set.batchId && e.resultId === set.id);
|
||||
@@ -420,6 +456,7 @@ class GridTable<T> extends Disposable implements IView {
|
||||
});
|
||||
this.dataProvider.dataRows = collection;
|
||||
this.table.updateRowCount();
|
||||
this.setupState();
|
||||
}
|
||||
|
||||
public onRemove() {
|
||||
@@ -513,12 +550,18 @@ class GridTable<T> extends Disposable implements IView {
|
||||
});
|
||||
|
||||
this.table.grid.onScroll.subscribe((e, data) => {
|
||||
if (!this.scrolled && this.state.scrollPosition && isInDOM(this.container)) {
|
||||
if (!this.visible) {
|
||||
// If the grid is not set up yet it can get scroll events resetting the top to 0px,
|
||||
// so ignore those events
|
||||
return;
|
||||
}
|
||||
if (!this.scrolled && (this.state.scrollPositionY || this.state.scrollPositionX) && isInDOM(this.container)) {
|
||||
this.scrolled = true;
|
||||
this.table.grid.scrollTo(this.state.scrollPosition);
|
||||
this.restoreScrollState();
|
||||
}
|
||||
if (this.state && isInDOM(this.container)) {
|
||||
this.state.scrollPosition = data.scrollTop;
|
||||
this.state.scrollPositionY = data.scrollTop;
|
||||
this.state.scrollPositionX = data.scrollLeft;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -527,8 +570,13 @@ class GridTable<T> extends Disposable implements IView {
|
||||
this.state.activeCell = this.table.grid.getActiveCell();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.setupState();
|
||||
private restoreScrollState() {
|
||||
if (this.state.scrollPositionX || this.state.scrollPositionY) {
|
||||
this.table.grid.scrollTo(this.state.scrollPositionY);
|
||||
this.table.grid.getContainerNode().children[3].scrollLeft = this.state.scrollPositionX;
|
||||
}
|
||||
}
|
||||
|
||||
private setupState() {
|
||||
@@ -537,20 +585,18 @@ class GridTable<T> extends Disposable implements IView {
|
||||
|
||||
this._register(this.state.onCanBeMaximizedChange(this.rebuildActionBar, this));
|
||||
|
||||
if (this.state.scrollPosition) {
|
||||
// most of the time this won't do anything
|
||||
this.table.grid.scrollTo(this.state.scrollPosition);
|
||||
// the problem here is that the scrolling state slickgrid uses
|
||||
// doesn't work with it offDOM.
|
||||
}
|
||||
this.restoreScrollState();
|
||||
|
||||
if (this.state.selection) {
|
||||
this.selectionModel.setSelectedRanges(this.state.selection);
|
||||
}
|
||||
// Setting the active cell resets the selection so save it here
|
||||
let savedSelection = this.state.selection;
|
||||
|
||||
if (this.state.activeCell) {
|
||||
this.table.setActiveCell(this.state.activeCell.row, this.state.activeCell.cell);
|
||||
}
|
||||
|
||||
if (savedSelection) {
|
||||
this.selectionModel.setSelectedRanges(savedSelection);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): GridTableState {
|
||||
@@ -601,6 +647,7 @@ class GridTable<T> extends Disposable implements IView {
|
||||
this.table.updateRowCount();
|
||||
}
|
||||
this.rowNumberColumn.updateRowCount(resultSet.rowCount);
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
private generateContext(cell?: Slick.Cell): IGridActionContext {
|
||||
|
||||
@@ -144,6 +144,7 @@ export class MessagePanel extends ViewletPanel {
|
||||
this.reset();
|
||||
this.queryRunnerDisposables.push(runner.onQueryStart(() => this.reset()));
|
||||
this.queryRunnerDisposables.push(runner.onMessage(e => this.onMessage(e)));
|
||||
this.onMessage(runner.messages);
|
||||
}
|
||||
|
||||
private onMessage(message: IResultMessage | IResultMessage[]) {
|
||||
|
||||
@@ -387,8 +387,12 @@ export class QueryModelService implements IQueryModelService {
|
||||
// We do not have a query runner for this editor, so create a new one
|
||||
// and map it to the results uri
|
||||
queryRunner = this._instantiationService.createInstance(QueryRunner, ownerUri);
|
||||
const resultSetEventType = 'resultSet';
|
||||
queryRunner.addListener(QREvents.RESULT_SET, resultSet => {
|
||||
this._fireQueryEvent(ownerUri, 'resultSet', resultSet);
|
||||
this._fireQueryEvent(ownerUri, resultSetEventType, resultSet);
|
||||
});
|
||||
queryRunner.onResultSetUpdate(resultSetSummary => {
|
||||
this._fireQueryEvent(ownerUri, resultSetEventType, resultSetSummary);
|
||||
});
|
||||
queryRunner.addListener(QREvents.BATCH_START, batch => {
|
||||
let link = undefined;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IQueryManagementService } from 'sql/parts/query/common/queryManagement'
|
||||
import * as Utils from 'sql/parts/connection/common/utils';
|
||||
import { SaveFormat } from 'sql/parts/grid/common/interfaces';
|
||||
import { echo, debounceEvent } from 'sql/base/common/event';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
@@ -26,7 +27,6 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ResultSerializer } from 'sql/parts/query/common/resultSerializer';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
|
||||
export interface IEditSessionReadyEvent {
|
||||
ownerUri: string;
|
||||
@@ -69,6 +69,7 @@ export default class QueryRunner extends Disposable {
|
||||
private _isExecuting: boolean = false;
|
||||
private _hasCompleted: boolean = false;
|
||||
private _batchSets: sqlops.BatchSummary[] = [];
|
||||
private _messages: sqlops.IResultMessage[] = [];
|
||||
private _eventEmitter = new EventEmitter();
|
||||
|
||||
private _isQueryPlan: boolean;
|
||||
@@ -77,40 +78,13 @@ export default class QueryRunner extends Disposable {
|
||||
public get planXml(): Thenable<string> { return this._planXml.promise; }
|
||||
|
||||
private _onMessage = this._register(new Emitter<sqlops.IResultMessage>());
|
||||
private _debouncedMessage = debounceEvent<sqlops.IResultMessage, sqlops.IResultMessage[]>(this._onMessage.event, (l, e) => {
|
||||
// on first run
|
||||
if (types.isUndefinedOrNull(l)) {
|
||||
return [e];
|
||||
} else {
|
||||
return l.concat(e);
|
||||
}
|
||||
});
|
||||
private _echoedMessages = echo(this._debouncedMessage.event);
|
||||
public readonly onMessage = this._echoedMessages.event;
|
||||
public readonly onMessage = this._onMessage.event;
|
||||
|
||||
private _onResultSet = this._register(new Emitter<sqlops.ResultSetSummary>());
|
||||
private _debouncedResultSet = debounceEvent<sqlops.ResultSetSummary, sqlops.ResultSetSummary[]>(this._onResultSet.event, (l, e) => {
|
||||
// on first run
|
||||
if (types.isUndefinedOrNull(l)) {
|
||||
return [e];
|
||||
} else {
|
||||
return l.concat(e);
|
||||
}
|
||||
});
|
||||
private _echoedResultSet = echo(this._debouncedResultSet.event);
|
||||
public readonly onResultSet = this._echoedResultSet.event;
|
||||
public readonly onResultSet = this._onResultSet.event;
|
||||
|
||||
private _onResultSetUpdate = this._register(new Emitter<sqlops.ResultSetSummary>());
|
||||
private _debouncedResultSetUpdate = debounceEvent<sqlops.ResultSetSummary, sqlops.ResultSetSummary[]>(this._onResultSetUpdate.event, (l, e) => {
|
||||
// on first run
|
||||
if (types.isUndefinedOrNull(l)) {
|
||||
return [e];
|
||||
} else {
|
||||
return l.concat(e);
|
||||
}
|
||||
});
|
||||
private _echoedResultSetUpdate = echo(this._debouncedResultSetUpdate.event);
|
||||
public readonly onResultSetUpdate = this._echoedResultSetUpdate.event;
|
||||
public readonly onResultSetUpdate = this._onResultSetUpdate.event;
|
||||
|
||||
private _onQueryStart = this._register(new Emitter<void>());
|
||||
public readonly onQueryStart: Event<void> = this._onQueryStart.event;
|
||||
@@ -153,8 +127,18 @@ export default class QueryRunner extends Disposable {
|
||||
return this._hasCompleted;
|
||||
}
|
||||
|
||||
get batchSets(): sqlops.BatchSummary[] {
|
||||
return this._batchSets;
|
||||
/**
|
||||
* For public use only, for private use, directly access the member
|
||||
*/
|
||||
public get batchSets(): sqlops.BatchSummary[] {
|
||||
return this._batchSets.slice(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* For public use only, for private use, directly access the member
|
||||
*/
|
||||
public get messages(): sqlops.IResultMessage[] {
|
||||
return this._messages.slice(0);
|
||||
}
|
||||
|
||||
// PUBLIC METHODS ======================================================
|
||||
@@ -202,10 +186,6 @@ export default class QueryRunner extends Disposable {
|
||||
if (this.isExecuting) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
this._echoedMessages.clear();
|
||||
this._echoedResultSet.clear();
|
||||
this._debouncedMessage.clear();
|
||||
this._debouncedResultSet.clear();
|
||||
this._planXml = new Deferred<string>();
|
||||
this._batchSets = [];
|
||||
this._hasCompleted = false;
|
||||
@@ -274,7 +254,7 @@ export default class QueryRunner extends Disposable {
|
||||
this._hasCompleted = true;
|
||||
this._batchSets = result.batchSummaries ? result.batchSummaries : [];
|
||||
|
||||
this.batchSets.map(batch => {
|
||||
this._batchSets.map(batch => {
|
||||
if (batch.selection) {
|
||||
batch.selection.startLine = batch.selection.startLine + this._resultLineOffset;
|
||||
batch.selection.endLine = batch.selection.endLine + this._resultLineOffset;
|
||||
@@ -291,6 +271,7 @@ export default class QueryRunner extends Disposable {
|
||||
isError: false,
|
||||
time: undefined
|
||||
};
|
||||
this._messages.push(message);
|
||||
|
||||
this._onQueryEnd.fire(timeStamp);
|
||||
this._onMessage.fire(message);
|
||||
@@ -312,7 +293,7 @@ export default class QueryRunner extends Disposable {
|
||||
batch.resultSetSummaries = [];
|
||||
|
||||
// Store the batch
|
||||
this.batchSets[batch.id] = batch;
|
||||
this._batchSets[batch.id] = batch;
|
||||
|
||||
let message = {
|
||||
// account for index by 1
|
||||
@@ -321,6 +302,7 @@ export default class QueryRunner extends Disposable {
|
||||
selection: batch.selection,
|
||||
isError: false
|
||||
};
|
||||
this._messages.push(message);
|
||||
this._eventEmitter.emit(EventType.BATCH_START, batch);
|
||||
this._onMessage.fire(message);
|
||||
this._onBatchStart.fire(batch);
|
||||
@@ -333,7 +315,7 @@ export default class QueryRunner extends Disposable {
|
||||
let batch: sqlops.BatchSummary = result.batchSummary;
|
||||
|
||||
// Store the batch again to get the rest of the data
|
||||
this.batchSets[batch.id] = batch;
|
||||
this._batchSets[batch.id] = batch;
|
||||
let executionTime = <number>(Utils.parseTimeString(batch.executionElapsed) || 0);
|
||||
this._totalElapsedMilliseconds += executionTime;
|
||||
if (executionTime > 0) {
|
||||
@@ -355,8 +337,8 @@ export default class QueryRunner extends Disposable {
|
||||
if (!resultSet.batchId) {
|
||||
// Missing the batchId. In this case, default to always using the first batch in the list
|
||||
// or create one in the case the DMP extension didn't obey the contract perfectly
|
||||
if (this.batchSets.length > 0) {
|
||||
batchSet = this.batchSets[0];
|
||||
if (this._batchSets.length > 0) {
|
||||
batchSet = this._batchSets[0];
|
||||
} else {
|
||||
batchSet = <sqlops.BatchSummary>{
|
||||
id: 0,
|
||||
@@ -364,10 +346,10 @@ export default class QueryRunner extends Disposable {
|
||||
hasError: false,
|
||||
resultSetSummaries: []
|
||||
};
|
||||
this.batchSets[0] = batchSet;
|
||||
this._batchSets[0] = batchSet;
|
||||
}
|
||||
} else {
|
||||
batchSet = this.batchSets[resultSet.batchId];
|
||||
batchSet = this._batchSets[resultSet.batchId];
|
||||
}
|
||||
// handle getting queryPlanxml if we need too
|
||||
if (this.isQueryPlan) {
|
||||
@@ -392,7 +374,7 @@ export default class QueryRunner extends Disposable {
|
||||
if (result && result.resultSetSummary) {
|
||||
let resultSet = result.resultSetSummary;
|
||||
let batchSet: sqlops.BatchSummary;
|
||||
batchSet = this.batchSets[resultSet.batchId];
|
||||
batchSet = this._batchSets[resultSet.batchId];
|
||||
// handle getting queryPlanxml if we need too
|
||||
if (this.isQueryPlan) {
|
||||
// check if this result has show plan, this needs work, it won't work for any other provider
|
||||
@@ -415,6 +397,7 @@ export default class QueryRunner extends Disposable {
|
||||
public handleMessage(obj: sqlops.QueryExecuteMessageParams): void {
|
||||
let message = obj.message;
|
||||
message.time = new Date(message.time).toLocaleTimeString();
|
||||
this._messages.push(message);
|
||||
|
||||
// Send the message to the results pane
|
||||
this._eventEmitter.emit(EventType.MESSAGE, message);
|
||||
@@ -434,10 +417,10 @@ export default class QueryRunner extends Disposable {
|
||||
};
|
||||
|
||||
return this._queryManagementService.getQueryRows(rowData).then(r => r, error => {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('query.gettingRowsFailedError', 'Something went wrong getting more rows: {0}', error)
|
||||
});
|
||||
// this._notificationService.notify({
|
||||
// severity: Severity.Error,
|
||||
// message: nls.localize('query.gettingRowsFailedError', 'Something went wrong getting more rows: {0}', error)
|
||||
// });
|
||||
return error;
|
||||
});
|
||||
}
|
||||
@@ -492,11 +475,11 @@ export default class QueryRunner extends Disposable {
|
||||
}
|
||||
resolve(result);
|
||||
}, error => {
|
||||
let errorMessage = nls.localize('query.moreRowsFailedError', 'Something went wrong getting more rows:');
|
||||
self._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: `${errorMessage} ${error}`
|
||||
});
|
||||
// let errorMessage = nls.localize('query.moreRowsFailedError', 'Something went wrong getting more rows:');
|
||||
// self._notificationService.notify({
|
||||
// severity: Severity.Error,
|
||||
// message: `${errorMessage} ${error}`
|
||||
// });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
@@ -636,7 +619,7 @@ export default class QueryRunner extends Disposable {
|
||||
|
||||
private getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] {
|
||||
let headers: string[] = undefined;
|
||||
let batchSummary: sqlops.BatchSummary = this.batchSets[batchId];
|
||||
let batchSummary: sqlops.BatchSummary = this._batchSets[batchId];
|
||||
if (batchSummary !== undefined) {
|
||||
let resultSetSummary = batchSummary.resultSetSummaries[resultId];
|
||||
headers = resultSetSummary.columnInfo.slice(range.fromCell, range.toCell + 1).map((info, i) => {
|
||||
@@ -669,6 +652,7 @@ export default class QueryRunner extends Disposable {
|
||||
time: undefined,
|
||||
isError: false
|
||||
};
|
||||
this._messages.push(message);
|
||||
// Send the message to the results pane
|
||||
this._onMessage.fire(message);
|
||||
}
|
||||
|
||||
@@ -97,15 +97,6 @@ export class WizardModal extends Modal {
|
||||
|
||||
messageChangeHandler(this._wizard.message);
|
||||
this._wizard.onMessageChange(message => messageChangeHandler(message));
|
||||
|
||||
this._wizard.pages.forEach((page, index) => {
|
||||
page.onValidityChanged(valid => {
|
||||
if (index === this._wizard.currentPage) {
|
||||
this._nextButton.enabled = this._wizard.nextButton.enabled && page.valid;
|
||||
this._doneButton.enabled = this._wizard.doneButton.enabled && page.valid;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requirePageValid: boolean = false): Button {
|
||||
@@ -198,6 +189,13 @@ export class WizardModal extends Modal {
|
||||
let currentPageValid = this._wizard.pages[this._wizard.currentPage].valid;
|
||||
this._nextButton.enabled = this._wizard.nextButton.enabled && currentPageValid;
|
||||
this._doneButton.enabled = this._wizard.doneButton.enabled && currentPageValid;
|
||||
|
||||
pageToShow.onValidityChanged(valid => {
|
||||
if (index === this._wizard.currentPage) {
|
||||
this._nextButton.enabled = this._wizard.nextButton.enabled && pageToShow.valid;
|
||||
this._doneButton.enabled = this._wizard.doneButton.enabled && pageToShow.valid;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setButtonsForPage(index: number) {
|
||||
|
||||
@@ -9,12 +9,13 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as platform from 'vs/platform/registry/common/platform';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
export const Extensions = {
|
||||
NotebookProviderContribution: 'notebook.providers'
|
||||
};
|
||||
|
||||
export interface NotebookProviderDescription {
|
||||
export interface NotebookProviderRegistration {
|
||||
provider: string;
|
||||
fileExtensions: string | string[];
|
||||
}
|
||||
@@ -54,42 +55,28 @@ let notebookContrib: IJSONSchema = {
|
||||
};
|
||||
|
||||
export interface INotebookProviderRegistry {
|
||||
registerNotebookProvider(provider: NotebookProviderDescription): void;
|
||||
getSupportedFileExtensions(): string[];
|
||||
getProviderForFileType(fileType: string): string;
|
||||
readonly registrations: NotebookProviderRegistration[];
|
||||
readonly onNewRegistration: Event<{ id: string, registration: NotebookProviderRegistration }>;
|
||||
|
||||
registerNotebookProvider(registration: NotebookProviderRegistration): void;
|
||||
}
|
||||
|
||||
class NotebookProviderRegistry implements INotebookProviderRegistry {
|
||||
private providerIdToProviders = new Map<string, NotebookProviderDescription>();
|
||||
private fileToProviders = new Map<string, NotebookProviderDescription>();
|
||||
private providerIdToRegistration = new Map<string, NotebookProviderRegistration>();
|
||||
private _onNewRegistration = new Emitter<{ id: string, registration: NotebookProviderRegistration }>();
|
||||
public readonly onNewRegistration: Event<{ id: string, registration: NotebookProviderRegistration }> = this._onNewRegistration.event;
|
||||
|
||||
registerNotebookProvider(provider: NotebookProviderDescription): void {
|
||||
registerNotebookProvider(registration: NotebookProviderRegistration): void {
|
||||
// Note: this method intentionally overrides default provider for a file type.
|
||||
// This means that any built-in provider will be overridden by registered extensions
|
||||
this.providerIdToProviders.set(provider.provider, provider);
|
||||
if (provider.fileExtensions) {
|
||||
if (Array.isArray<string>(provider.fileExtensions)) {
|
||||
for (let fileType of provider.fileExtensions) {
|
||||
this.addFileProvider(fileType, provider);
|
||||
}
|
||||
} else {
|
||||
this.addFileProvider(provider.fileExtensions, provider);
|
||||
}
|
||||
}
|
||||
this.providerIdToRegistration.set(registration.provider, registration);
|
||||
this._onNewRegistration.fire( { id: registration.provider, registration: registration });
|
||||
}
|
||||
|
||||
private addFileProvider(fileType: string, provider: NotebookProviderDescription) {
|
||||
this.fileToProviders.set(fileType.toUpperCase(), provider);
|
||||
}
|
||||
|
||||
getSupportedFileExtensions(): string[] {
|
||||
return Array.from(this.fileToProviders.keys());
|
||||
}
|
||||
|
||||
getProviderForFileType(fileType: string): string {
|
||||
fileType = fileType.toUpperCase();
|
||||
let provider = this.fileToProviders.get(fileType);
|
||||
return provider ? provider.provider : undefined;
|
||||
public get registrations(): NotebookProviderRegistration[] {
|
||||
let registrationArray: NotebookProviderRegistration[] = [];
|
||||
this.providerIdToRegistration.forEach(p => registrationArray.push(p));
|
||||
return registrationArray;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,15 +84,15 @@ const notebookProviderRegistry = new NotebookProviderRegistry();
|
||||
platform.Registry.add(Extensions.NotebookProviderContribution, notebookProviderRegistry);
|
||||
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<NotebookProviderDescription | NotebookProviderDescription[]>(Extensions.NotebookProviderContribution, [], notebookContrib).setHandler(extensions => {
|
||||
ExtensionsRegistry.registerExtensionPoint<NotebookProviderRegistration | NotebookProviderRegistration[]>(Extensions.NotebookProviderContribution, [], notebookContrib).setHandler(extensions => {
|
||||
|
||||
function handleExtension(contrib: NotebookProviderDescription, extension: IExtensionPointUser<any>) {
|
||||
function handleExtension(contrib: NotebookProviderRegistration, extension: IExtensionPointUser<any>) {
|
||||
notebookProviderRegistry.registerNotebookProvider(contrib);
|
||||
}
|
||||
|
||||
for (let extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<NotebookProviderDescription>(value)) {
|
||||
if (Array.isArray<NotebookProviderRegistration>(value)) {
|
||||
for (let command of value) {
|
||||
handleExtension(command, extension);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { NotebookInput } from 'sql/parts/notebook/notebookInput';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { ICellModel, INotebookModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
|
||||
export const SERVICE_ID = 'notebookService';
|
||||
export const INotebookService = createDecorator<INotebookService>(SERVICE_ID);
|
||||
@@ -26,9 +27,12 @@ export const DEFAULT_NOTEBOOK_FILETYPE = 'IPYNB';
|
||||
export interface INotebookService {
|
||||
_serviceBrand: any;
|
||||
|
||||
onNotebookEditorAdd: Event<INotebookEditor>;
|
||||
onNotebookEditorRemove: Event<INotebookEditor>;
|
||||
readonly onNotebookEditorAdd: Event<INotebookEditor>;
|
||||
readonly onNotebookEditorRemove: Event<INotebookEditor>;
|
||||
onNotebookEditorRename: Event<INotebookEditor>;
|
||||
|
||||
readonly isRegistrationComplete: boolean;
|
||||
readonly registrationComplete: Promise<void>;
|
||||
/**
|
||||
* Register a metadata provider
|
||||
*/
|
||||
@@ -39,6 +43,10 @@ export interface INotebookService {
|
||||
*/
|
||||
unregisterProvider(providerId: string): void;
|
||||
|
||||
getSupportedFileExtensions(): string[];
|
||||
|
||||
getProviderForFileType(fileType: string): string;
|
||||
|
||||
/**
|
||||
* Initializes and returns a Notebook manager that can handle all important calls to open, display, and
|
||||
* run cells in a notebook.
|
||||
@@ -57,6 +65,8 @@ export interface INotebookService {
|
||||
shutdown(): void;
|
||||
|
||||
getMimeRegistry(): RenderMimeRegistry;
|
||||
|
||||
renameNotebookEditor(oldUri: URI, newUri: URI, currentEditor: INotebookEditor): void;
|
||||
}
|
||||
|
||||
export interface INotebookProvider {
|
||||
@@ -84,6 +94,8 @@ export interface INotebookParams extends IBootstrapParams {
|
||||
export interface INotebookEditor {
|
||||
readonly notebookParams: INotebookParams;
|
||||
readonly id: string;
|
||||
readonly cells?: ICellModel[];
|
||||
readonly modelReady: Promise<INotebookModel>;
|
||||
isDirty(): boolean;
|
||||
isActive(): boolean;
|
||||
isVisible(): boolean;
|
||||
|
||||
@@ -18,41 +18,143 @@ import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry';
|
||||
import { standardRendererFactories } from 'sql/parts/notebook/outputs/factories';
|
||||
import { LocalContentManager } from 'sql/services/notebook/localContentManager';
|
||||
import { SessionManager } from 'sql/services/notebook/sessionManager';
|
||||
import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry';
|
||||
import { Extensions, INotebookProviderRegistry, NotebookProviderRegistration } from 'sql/services/notebook/notebookRegistry';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Memento } from 'vs/workbench/common/memento';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionManagementService, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
|
||||
export interface NotebookProviderProperties {
|
||||
provider: string;
|
||||
fileExtensions: string[];
|
||||
}
|
||||
|
||||
export class NotebookService implements INotebookService {
|
||||
interface NotebookProviderCache {
|
||||
[id: string]: NotebookProviderProperties;
|
||||
}
|
||||
|
||||
interface NotebookProvidersMemento {
|
||||
notebookProviderCache: NotebookProviderCache;
|
||||
}
|
||||
|
||||
const notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
||||
|
||||
class ProviderDescriptor {
|
||||
private _instanceReady = new Deferred<INotebookProvider>();
|
||||
constructor(private providerId: string, private _instance?: INotebookProvider) {
|
||||
if (_instance) {
|
||||
this._instanceReady.resolve(_instance);
|
||||
}
|
||||
}
|
||||
|
||||
public get instanceReady(): Promise<INotebookProvider> {
|
||||
return this._instanceReady.promise;
|
||||
}
|
||||
|
||||
public get instance(): INotebookProvider {
|
||||
return this._instance;
|
||||
}
|
||||
public set instance(value: INotebookProvider) {
|
||||
this._instance = value;
|
||||
this._instanceReady.resolve(value);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotebookService extends Disposable implements INotebookService {
|
||||
_serviceBrand: any;
|
||||
|
||||
private _memento = new Memento('notebookProviders');
|
||||
private _mimeRegistry: RenderMimeRegistry;
|
||||
private _providers: Map<string, INotebookProvider> = new Map();
|
||||
private _providers: Map<string, ProviderDescriptor> = new Map();
|
||||
private _managers: Map<string, INotebookManager> = new Map();
|
||||
private _onNotebookEditorAdd = new Emitter<INotebookEditor>();
|
||||
private _onNotebookEditorRemove = new Emitter<INotebookEditor>();
|
||||
private _onCellChanged = new Emitter<INotebookEditor>();
|
||||
private _onNotebookEditorRename = new Emitter<INotebookEditor>();
|
||||
private _editors = new Map<string, INotebookEditor>();
|
||||
private _fileToProviders = new Map<string, NotebookProviderRegistration>();
|
||||
private _registrationComplete = new Deferred<void>();
|
||||
private _isRegistrationComplete = false;
|
||||
|
||||
constructor() {
|
||||
constructor(
|
||||
@IStorageService private _storageService: IStorageService,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IExtensionManagementService extensionManagementService: IExtensionManagementService
|
||||
) {
|
||||
super();
|
||||
this._register(notebookRegistry.onNewRegistration(this.updateRegisteredProviders, this));
|
||||
this.registerDefaultProvider();
|
||||
|
||||
if (extensionService) {
|
||||
extensionService.whenInstalledExtensionsRegistered().then(() => {
|
||||
this.cleanupProviders();
|
||||
this._isRegistrationComplete = true;
|
||||
this._registrationComplete.resolve();
|
||||
});
|
||||
}
|
||||
if (extensionManagementService) {
|
||||
this._register(extensionManagementService.onDidUninstallExtension(({ identifier }) => this.removeContributedProvidersFromCache(identifier, extensionService)));
|
||||
}
|
||||
}
|
||||
|
||||
private registerDefaultProvider() {
|
||||
let defaultProvider = new BuiltinProvider();
|
||||
this.registerProvider(defaultProvider.providerId, defaultProvider);
|
||||
let registry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
||||
registry.registerNotebookProvider({
|
||||
provider: defaultProvider.providerId,
|
||||
fileExtensions: DEFAULT_NOTEBOOK_FILETYPE
|
||||
});
|
||||
private updateRegisteredProviders(p: { id: string; registration: NotebookProviderRegistration; }) {
|
||||
let registration = p.registration;
|
||||
|
||||
if (!this._providers.has(p.id)) {
|
||||
this._providers.set(p.id, new ProviderDescriptor(p.id));
|
||||
}
|
||||
if (registration.fileExtensions) {
|
||||
if (Array.isArray<string>(registration.fileExtensions)) {
|
||||
for (let fileType of registration.fileExtensions) {
|
||||
this.addFileProvider(fileType, registration);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.addFileProvider(registration.fileExtensions, registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerProvider(providerId: string, provider: INotebookProvider): void {
|
||||
this._providers.set(providerId, provider);
|
||||
registerProvider(providerId: string, instance: INotebookProvider): void {
|
||||
let providerDescriptor = this._providers.get(providerId);
|
||||
if (providerDescriptor) {
|
||||
// Update, which will resolve the promise for anyone waiting on the instance to be registered
|
||||
providerDescriptor.instance = instance;
|
||||
} else {
|
||||
this._providers.set(providerId, new ProviderDescriptor(providerId, instance));
|
||||
}
|
||||
}
|
||||
|
||||
unregisterProvider(providerId: string): void {
|
||||
this._providers.delete(providerId);
|
||||
}
|
||||
|
||||
get isRegistrationComplete(): boolean {
|
||||
return this._isRegistrationComplete;
|
||||
}
|
||||
|
||||
get registrationComplete(): Promise<void> {
|
||||
return this._registrationComplete.promise;
|
||||
}
|
||||
|
||||
private addFileProvider(fileType: string, provider: NotebookProviderRegistration) {
|
||||
this._fileToProviders.set(fileType.toUpperCase(), provider);
|
||||
}
|
||||
|
||||
getSupportedFileExtensions(): string[] {
|
||||
return Array.from(this._fileToProviders.keys());
|
||||
}
|
||||
|
||||
getProviderForFileType(fileType: string): string {
|
||||
fileType = fileType.toUpperCase();
|
||||
let provider = this._fileToProviders.get(fileType);
|
||||
return provider ? provider.provider : undefined;
|
||||
}
|
||||
|
||||
public shutdown(): void {
|
||||
this._managers.forEach(manager => {
|
||||
if (manager.serverManager) {
|
||||
@@ -83,6 +185,13 @@ export class NotebookService implements INotebookService {
|
||||
get onNotebookEditorRemove(): Event<INotebookEditor> {
|
||||
return this._onNotebookEditorRemove.event;
|
||||
}
|
||||
get onCellChanged(): Event<INotebookEditor> {
|
||||
return this._onCellChanged.event;
|
||||
}
|
||||
|
||||
get onNotebookEditorRename(): Event<INotebookEditor> {
|
||||
return this._onNotebookEditorRename.event;
|
||||
}
|
||||
|
||||
addNotebookEditor(editor: INotebookEditor): void {
|
||||
this._editors.set(editor.id, editor);
|
||||
@@ -103,32 +212,70 @@ export class NotebookService implements INotebookService {
|
||||
return editors;
|
||||
}
|
||||
|
||||
private sendNotebookCloseToProvider(editor: INotebookEditor) {
|
||||
renameNotebookEditor(oldUri: URI, newUri: URI, currentEditor: INotebookEditor): void {
|
||||
let oldUriKey = oldUri.toString();
|
||||
if(this._editors.has(oldUriKey))
|
||||
{
|
||||
this._editors.delete(oldUriKey);
|
||||
currentEditor.notebookParams.notebookUri = newUri;
|
||||
this._editors.set(newUri.toString(), currentEditor);
|
||||
this._onNotebookEditorRename.fire(currentEditor);
|
||||
}
|
||||
}
|
||||
|
||||
private sendNotebookCloseToProvider(editor: INotebookEditor): void {
|
||||
let notebookUri = editor.notebookParams.notebookUri;
|
||||
let uriString = notebookUri.toString();
|
||||
let manager = this._managers.get(uriString);
|
||||
if (manager) {
|
||||
// As we have a manager, we can assume provider is ready
|
||||
this._managers.delete(uriString);
|
||||
let provider = this._providers.get(manager.providerId);
|
||||
provider.handleNotebookClosed(notebookUri);
|
||||
provider.instance.handleNotebookClosed(notebookUri);
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private doWithProvider<T>(providerId: string, op: (provider: INotebookProvider) => Thenable<T>): Thenable<T> {
|
||||
private async doWithProvider<T>(providerId: string, op: (provider: INotebookProvider) => Thenable<T>): Promise<T> {
|
||||
// Make sure the provider exists before attempting to retrieve accounts
|
||||
let provider: INotebookProvider;
|
||||
if (this._providers.has(providerId)) {
|
||||
provider = this._providers.get(providerId);
|
||||
}
|
||||
else {
|
||||
provider = this._providers.get(DEFAULT_NOTEBOOK_PROVIDER);
|
||||
let provider: INotebookProvider = await this.getProviderInstance(providerId);
|
||||
return op(provider);
|
||||
}
|
||||
|
||||
private async getProviderInstance(providerId: string, timeout?: number): Promise<INotebookProvider> {
|
||||
let providerDescriptor = this._providers.get(providerId);
|
||||
let instance: INotebookProvider;
|
||||
|
||||
// Try get from actual provider, waiting on its registration
|
||||
if (providerDescriptor) {
|
||||
if (!providerDescriptor.instance) {
|
||||
instance = await this.waitOnProviderAvailability(providerDescriptor);
|
||||
} else {
|
||||
instance = providerDescriptor.instance;
|
||||
}
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
return Promise.reject(new Error(localize('notebookServiceNoProvider', 'Notebook provider does not exist'))).then();
|
||||
// Fall back to default if this failed
|
||||
if (!instance) {
|
||||
providerDescriptor = this._providers.get(DEFAULT_NOTEBOOK_PROVIDER);
|
||||
instance = providerDescriptor ? providerDescriptor.instance : undefined;
|
||||
}
|
||||
return op(provider);
|
||||
|
||||
// Should never happen, but if default wasn't registered we should throw
|
||||
if (!instance) {
|
||||
throw new Error(localize('notebookServiceNoProvider', 'Notebook provider does not exist'));
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private waitOnProviderAvailability(providerDescriptor: ProviderDescriptor, timeout?: number): Promise<INotebookProvider> {
|
||||
// Wait up to 10 seconds for the provider to be registered
|
||||
timeout = timeout || 10000;
|
||||
let promises: Promise<INotebookProvider>[] = [
|
||||
providerDescriptor.instanceReady,
|
||||
new Promise<INotebookProvider>((resolve, reject) => setTimeout(() => resolve(), timeout))
|
||||
];
|
||||
return Promise.race(promises);
|
||||
}
|
||||
|
||||
//Returns an instantiation of RenderMimeRegistry class
|
||||
@@ -140,6 +287,41 @@ export class NotebookService implements INotebookService {
|
||||
}
|
||||
return this._mimeRegistry;
|
||||
}
|
||||
|
||||
private get providersMemento(): NotebookProvidersMemento {
|
||||
return this._memento.getMemento(this._storageService) as NotebookProvidersMemento;
|
||||
}
|
||||
|
||||
private cleanupProviders(): void {
|
||||
let knownProviders = Object.keys(notebookRegistry.registrations);
|
||||
let cache = this.providersMemento.notebookProviderCache;
|
||||
for (let key in cache) {
|
||||
if (!knownProviders.includes(key)) {
|
||||
this._providers.delete(key);
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private registerDefaultProvider() {
|
||||
let defaultProvider = new BuiltinProvider();
|
||||
this.registerProvider(defaultProvider.providerId, defaultProvider);
|
||||
notebookRegistry.registerNotebookProvider({
|
||||
provider: defaultProvider.providerId,
|
||||
fileExtensions: DEFAULT_NOTEBOOK_FILETYPE
|
||||
});
|
||||
}
|
||||
|
||||
private removeContributedProvidersFromCache(identifier: IExtensionIdentifier, extensionService: IExtensionService) {
|
||||
let extensionid = getIdFromLocalExtensionId(identifier.id);
|
||||
extensionService.getExtensions().then(i => {
|
||||
let extension = i.find(c => c.id === extensionid);
|
||||
if (extension && extension.contributes['notebookProvider']) {
|
||||
let id = extension.contributes['notebookProvider'].providerId;
|
||||
delete this.providersMemento.notebookProviderCache[id];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class BuiltinProvider implements INotebookProvider {
|
||||
|
||||
3
src/sql/sqlops.proposed.d.ts
vendored
3
src/sql/sqlops.proposed.d.ts
vendored
@@ -1548,6 +1548,7 @@ declare module 'sqlops' {
|
||||
|
||||
export interface NotebookCell {
|
||||
contents: ICellContents;
|
||||
uri?: vscode.Uri;
|
||||
}
|
||||
|
||||
export interface NotebookShowOptions {
|
||||
@@ -1593,7 +1594,7 @@ declare module 'sqlops' {
|
||||
/**
|
||||
* The new value for the [notebook documents's cells](#NotebookDocument.cells).
|
||||
*/
|
||||
cell: NotebookCell[];
|
||||
cells: NotebookCell[];
|
||||
/**
|
||||
* The [change kind](#TextEditorSelectionChangeKind) which has triggered this
|
||||
* event. Can be `undefined`.
|
||||
|
||||
@@ -24,11 +24,6 @@ export class ExtHostNotebook implements ExtHostNotebookShape {
|
||||
|
||||
private readonly _proxy: MainThreadNotebookShape;
|
||||
private _adapters = new Map<number, Adapter>();
|
||||
private _onDidOpenNotebook = new Emitter<sqlops.nb.NotebookDocument>();
|
||||
private _onDidChangeNotebookCell = new Emitter<sqlops.nb.NotebookCellChangeEvent>();
|
||||
|
||||
public readonly onDidOpenNotebookDocument: Event<sqlops.nb.NotebookDocument> = this._onDidOpenNotebook.event;
|
||||
public readonly onDidChangeNotebookCell: Event<sqlops.nb.NotebookCellChangeEvent> = this._onDidChangeNotebookCell.event;
|
||||
|
||||
// Notebook URI to manager lookup.
|
||||
constructor(_mainContext: IMainContext) {
|
||||
|
||||
@@ -12,22 +12,20 @@ import { ok } from 'vs/base/common/assert';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
import { MainThreadNotebookDocumentsAndEditorsShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
|
||||
import { MainThreadNotebookDocumentsAndEditorsShape, INotebookModelChangedData } from 'sql/workbench/api/node/sqlExtHost.protocol';
|
||||
import { CellRange } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
|
||||
|
||||
export class ExtHostNotebookDocumentData implements IDisposable {
|
||||
private _document: sqlops.nb.NotebookDocument;
|
||||
private _cells: sqlops.nb.NotebookCell[];
|
||||
private _isDisposed: boolean = false;
|
||||
|
||||
constructor(private readonly _proxy: MainThreadNotebookDocumentsAndEditorsShape,
|
||||
private readonly _uri: URI,
|
||||
private readonly _providerId: string,
|
||||
private _isDirty: boolean
|
||||
private _providerId: string,
|
||||
private _isDirty: boolean,
|
||||
private _cells: sqlops.nb.NotebookCell[]
|
||||
) {
|
||||
// TODO add cell mapping support
|
||||
this._cells = [];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -66,6 +64,14 @@ export class ExtHostNotebookDocumentData implements IDisposable {
|
||||
|
||||
}
|
||||
|
||||
public onModelChanged(data: INotebookModelChangedData) {
|
||||
if (data) {
|
||||
this._isDirty = data.isDirty;
|
||||
this._cells = data.cells;
|
||||
this._providerId = data.providerId;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- range math
|
||||
|
||||
private _validateRange(range: sqlops.nb.CellRange): sqlops.nb.CellRange {
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as vscode from 'vscode';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { dispose } from 'vs/base/common/lifecycle';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import URI, { UriComponents } from 'vs/base/common/uri';
|
||||
import { Disposable } from 'vs/workbench/api/node/extHostTypes';
|
||||
import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters';
|
||||
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
|
||||
@@ -17,7 +17,7 @@ import { ok } from 'vs/base/common/assert';
|
||||
|
||||
import {
|
||||
SqlMainContext, INotebookDocumentsAndEditorsDelta, ExtHostNotebookDocumentsAndEditorsShape,
|
||||
MainThreadNotebookDocumentsAndEditorsShape, INotebookShowOptions
|
||||
MainThreadNotebookDocumentsAndEditorsShape, INotebookShowOptions, INotebookModelChangedData
|
||||
} from 'sql/workbench/api/node/sqlExtHost.protocol';
|
||||
import { ExtHostNotebookDocumentData } from 'sql/workbench/api/node/extHostNotebookDocumentData';
|
||||
import { ExtHostNotebookEditor } from 'sql/workbench/api/node/extHostNotebookEditor';
|
||||
@@ -33,15 +33,16 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume
|
||||
private readonly _editors = new Map<string, ExtHostNotebookEditor>();
|
||||
private readonly _documents = new Map<string, ExtHostNotebookDocumentData>();
|
||||
|
||||
private readonly _onDidAddDocuments = new Emitter<ExtHostNotebookDocumentData[]>();
|
||||
private readonly _onDidRemoveDocuments = new Emitter<ExtHostNotebookDocumentData[]>();
|
||||
private readonly _onDidChangeVisibleNotebookEditors = new Emitter<ExtHostNotebookEditor[]>();
|
||||
private readonly _onDidChangeActiveNotebookEditor = new Emitter<ExtHostNotebookEditor>();
|
||||
private _onDidOpenNotebook = new Emitter<sqlops.nb.NotebookDocument>();
|
||||
private _onDidChangeNotebookCell = new Emitter<sqlops.nb.NotebookCellChangeEvent>();
|
||||
|
||||
readonly onDidAddDocuments: Event<ExtHostNotebookDocumentData[]> = this._onDidAddDocuments.event;
|
||||
readonly onDidRemoveDocuments: Event<ExtHostNotebookDocumentData[]> = this._onDidRemoveDocuments.event;
|
||||
readonly onDidChangeVisibleNotebookEditors: Event<ExtHostNotebookEditor[]> = this._onDidChangeVisibleNotebookEditors.event;
|
||||
readonly onDidChangeActiveNotebookEditor: Event<ExtHostNotebookEditor> = this._onDidChangeActiveNotebookEditor.event;
|
||||
readonly onDidOpenNotebookDocument: Event<sqlops.nb.NotebookDocument> = this._onDidOpenNotebook.event;
|
||||
readonly onDidChangeNotebookCell: Event<sqlops.nb.NotebookCellChangeEvent> = this._onDidChangeNotebookCell.event;
|
||||
|
||||
|
||||
constructor(
|
||||
private readonly _mainContext: IMainContext,
|
||||
@@ -81,7 +82,8 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume
|
||||
this._proxy,
|
||||
resource,
|
||||
data.providerId,
|
||||
data.isDirty
|
||||
data.isDirty,
|
||||
data.cells
|
||||
);
|
||||
this._documents.set(resource.toString(), documentData);
|
||||
addedDocuments.push(documentData);
|
||||
@@ -122,11 +124,11 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume
|
||||
dispose(removedEditors);
|
||||
|
||||
// now that the internal state is complete, fire events
|
||||
if (delta.removedDocuments) {
|
||||
this._onDidRemoveDocuments.fire(removedDocuments);
|
||||
if (removedDocuments) {
|
||||
// TODO add doc close event
|
||||
}
|
||||
if (delta.addedDocuments) {
|
||||
this._onDidAddDocuments.fire(addedDocuments);
|
||||
if (addedDocuments) {
|
||||
addedDocuments.forEach(d => this._onDidOpenNotebook.fire(d.document));
|
||||
}
|
||||
|
||||
if (delta.removedEditors || delta.addedEditors) {
|
||||
@@ -136,6 +138,21 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume
|
||||
this._onDidChangeActiveNotebookEditor.fire(this.getActiveEditor());
|
||||
}
|
||||
}
|
||||
|
||||
$acceptModelChanged(uriComponents: UriComponents, e: INotebookModelChangedData): void {
|
||||
const uri = URI.revive(uriComponents);
|
||||
const strURL = uri.toString();
|
||||
let data = this._documents.get(strURL);
|
||||
if (data) {
|
||||
data.onModelChanged(e);
|
||||
this._onDidChangeNotebookCell.fire({
|
||||
cells: data.document.cells,
|
||||
notebook: data.document,
|
||||
kind: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Extension accessible methods
|
||||
|
||||
@@ -5,19 +5,22 @@
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as util from 'util';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import URI, { UriComponents } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IExtHostContext, IUndoStopOptions } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
|
||||
import { viewColumnToEditorGroup } from 'vs/workbench/api/shared/editor';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
import {
|
||||
SqlMainContext, MainThreadNotebookDocumentsAndEditorsShape, SqlExtHostContext, ExtHostNotebookDocumentsAndEditorsShape,
|
||||
INotebookDocumentsAndEditorsDelta, INotebookEditorAddData, INotebookShowOptions, INotebookModelAddedData
|
||||
INotebookDocumentsAndEditorsDelta, INotebookEditorAddData, INotebookShowOptions, INotebookModelAddedData, INotebookModelChangedData
|
||||
} from 'sql/workbench/api/node/sqlExtHost.protocol';
|
||||
import { NotebookInputModel, NotebookInput } from 'sql/parts/notebook/notebookInput';
|
||||
import { INotebookService, INotebookEditor } from 'sql/services/notebook/notebookService';
|
||||
@@ -25,11 +28,17 @@ import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { disposed } from 'vs/base/common/errors';
|
||||
import { ICellModel, NotebookContentChange } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
|
||||
class MainThreadNotebookEditor extends Disposable {
|
||||
private _contentChangedEmitter = new Emitter<NotebookContentChange>();
|
||||
public readonly contentChanged: Event<NotebookContentChange> = this._contentChangedEmitter.event;
|
||||
|
||||
constructor(public readonly editor: INotebookEditor) {
|
||||
super();
|
||||
editor.modelReady.then(model => {
|
||||
this._register(model.contentChanged((e) => this._contentChangedEmitter.fire(e)));
|
||||
});
|
||||
}
|
||||
|
||||
public get uri(): URI {
|
||||
@@ -48,6 +57,10 @@ class MainThreadNotebookEditor extends Disposable {
|
||||
return this.editor.notebookParams.providerId;
|
||||
}
|
||||
|
||||
public get cells(): ICellModel[] {
|
||||
return this.editor.cells;
|
||||
}
|
||||
|
||||
public save(): Thenable<boolean> {
|
||||
return this.editor.save();
|
||||
}
|
||||
@@ -205,6 +218,7 @@ class MainThreadNotebookDocumentAndEditorStateComputer extends Disposable {
|
||||
this._register(this._editorService.onDidVisibleEditorsChange(this._updateState, this));
|
||||
this._register(this._notebookService.onNotebookEditorAdd(this._onDidAddEditor, this));
|
||||
this._register(this._notebookService.onNotebookEditorRemove(this._onDidRemoveEditor, this));
|
||||
this._register(this._notebookService.onNotebookEditorRename(this._onDidRenameEditor, this));
|
||||
|
||||
this._updateState();
|
||||
}
|
||||
@@ -219,6 +233,11 @@ class MainThreadNotebookDocumentAndEditorStateComputer extends Disposable {
|
||||
this._updateState();
|
||||
}
|
||||
|
||||
private _onDidRenameEditor(e: INotebookEditor): void {
|
||||
this._updateState();
|
||||
//TODO: Close editor and open it
|
||||
}
|
||||
|
||||
private _updateState(): void {
|
||||
// editor
|
||||
const editors = new Map<string, INotebookEditor>();
|
||||
@@ -246,12 +265,13 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
|
||||
|
||||
private _proxy: ExtHostNotebookDocumentsAndEditorsShape;
|
||||
private _notebookEditors = new Map<string, MainThreadNotebookEditor>();
|
||||
|
||||
private _modelToDisposeMap = new Map<string, IDisposable>();
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IEditorService private _editorService: IEditorService,
|
||||
@IEditorGroupsService private _editorGroupService: IEditorGroupsService
|
||||
@IEditorGroupsService private _editorGroupService: IEditorGroupsService,
|
||||
@INotebookService private readonly _notebookService: INotebookService
|
||||
) {
|
||||
super();
|
||||
if (extHostContext) {
|
||||
@@ -293,12 +313,13 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
|
||||
preserveFocus: options.preserveFocus,
|
||||
pinned: !options.preview
|
||||
};
|
||||
let model = new NotebookInputModel(uri, undefined, false, undefined);
|
||||
let trusted = uri.scheme === Schemas.untitled;
|
||||
let model = new NotebookInputModel(uri, undefined, trusted, undefined);
|
||||
let providerId = options.providerId;
|
||||
if(!providerId)
|
||||
{
|
||||
// Ensure there is always a sensible provider ID for this file type
|
||||
providerId = getProviderForFileName(uri.fsPath);
|
||||
providerId = getProviderForFileName(uri.fsPath, this._notebookService);
|
||||
}
|
||||
|
||||
model.providerId = providerId;
|
||||
@@ -389,8 +410,34 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
|
||||
|
||||
if (!empty) {
|
||||
this._proxy.$acceptDocumentsAndEditorsDelta(extHostDelta);
|
||||
this.processRemovedDocs(removedDocuments);
|
||||
this.processAddedDocs(addedEditors);
|
||||
}
|
||||
}
|
||||
processRemovedDocs(removedDocuments: URI[]): void {
|
||||
if (!removedDocuments) {
|
||||
return;
|
||||
}
|
||||
removedDocuments.forEach(removedDoc => {
|
||||
let listener = this._modelToDisposeMap.get(removedDoc.toString());
|
||||
if (listener) {
|
||||
listener.dispose();
|
||||
this._modelToDisposeMap.delete(removedDoc.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
processAddedDocs(addedEditors: MainThreadNotebookEditor[]): any {
|
||||
if (!addedEditors) {
|
||||
return;
|
||||
}
|
||||
addedEditors.forEach(editor => {
|
||||
let modelUrl = editor.uri;
|
||||
this._modelToDisposeMap.set(editor.uri.toString(), editor.contentChanged((e) => {
|
||||
this._proxy.$acceptModelChanged(modelUrl, this._toNotebookChangeData(e, editor));
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private _toNotebookEditorAddData(editor: MainThreadNotebookEditor): INotebookEditorAddData {
|
||||
let addData: INotebookEditorAddData = {
|
||||
@@ -405,8 +452,54 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements
|
||||
let addData: INotebookModelAddedData = {
|
||||
uri: editor.uri,
|
||||
isDirty: editor.isDirty,
|
||||
providerId: editor.providerId
|
||||
providerId: editor.providerId,
|
||||
cells: this.convertCellModelToNotebookCell(editor.cells)
|
||||
};
|
||||
return addData;
|
||||
}
|
||||
|
||||
private _toNotebookChangeData(e: NotebookContentChange, editor: MainThreadNotebookEditor): INotebookModelChangedData {
|
||||
let changeData: INotebookModelChangedData = {
|
||||
// Note: we just send all cells for now, not a diff
|
||||
cells: this.convertCellModelToNotebookCell(editor.cells),
|
||||
isDirty: e.isDirty,
|
||||
providerId: editor.providerId,
|
||||
uri: editor.uri
|
||||
};
|
||||
return changeData;
|
||||
}
|
||||
|
||||
private convertCellModelToNotebookCell(cells: ICellModel | ICellModel[]): sqlops.nb.NotebookCell[] {
|
||||
let notebookCells: sqlops.nb.NotebookCell[] = [];
|
||||
if (Array.isArray(cells)) {
|
||||
for (let cell of cells) {
|
||||
notebookCells.push({
|
||||
uri: cell.cellUri,
|
||||
contents: {
|
||||
cell_type: cell.cellType,
|
||||
execution_count: undefined,
|
||||
metadata: {
|
||||
language: cell.language
|
||||
},
|
||||
source: undefined
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
notebookCells.push({
|
||||
uri: cells.cellUri,
|
||||
contents: {
|
||||
cell_type: cells.cellType,
|
||||
execution_count: undefined,
|
||||
metadata: {
|
||||
language: cells.language
|
||||
},
|
||||
source: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
return notebookCells;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,10 +432,10 @@ export function createApiFactory(
|
||||
return extHostNotebookDocumentsAndEditors.getAllEditors();
|
||||
},
|
||||
get onDidOpenNotebookDocument() {
|
||||
return extHostNotebook.onDidOpenNotebookDocument;
|
||||
return extHostNotebookDocumentsAndEditors.onDidOpenNotebookDocument;
|
||||
},
|
||||
get onDidChangeNotebookCell() {
|
||||
return extHostNotebook.onDidChangeNotebookCell;
|
||||
return extHostNotebookDocumentsAndEditors.onDidChangeNotebookCell;
|
||||
},
|
||||
showNotebookDocument(uri: vscode.Uri, showOptions: sqlops.nb.NotebookShowOptions) {
|
||||
return extHostNotebookDocumentsAndEditors.showNotebookDocument(uri, showOptions);
|
||||
|
||||
@@ -797,6 +797,14 @@ export interface INotebookModelAddedData {
|
||||
uri: UriComponents;
|
||||
providerId: string;
|
||||
isDirty: boolean;
|
||||
cells: sqlops.nb.NotebookCell[];
|
||||
}
|
||||
|
||||
export interface INotebookModelChangedData {
|
||||
uri: UriComponents;
|
||||
providerId: string;
|
||||
isDirty: boolean;
|
||||
cells: sqlops.nb.NotebookCell[];
|
||||
}
|
||||
|
||||
export interface INotebookEditorAddData {
|
||||
@@ -815,6 +823,7 @@ export interface INotebookShowOptions {
|
||||
|
||||
export interface ExtHostNotebookDocumentsAndEditorsShape {
|
||||
$acceptDocumentsAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void;
|
||||
$acceptModelChanged(strURL: UriComponents, e: INotebookModelChangedData);
|
||||
}
|
||||
|
||||
export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { nb, IConnectionProfile } from 'sqlops';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { INotebookModel, ICellModel, IClientSession, IDefaultConnection } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { INotebookModel, ICellModel, IClientSession, IDefaultConnection, NotebookContentChange } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { NotebookChangeType, CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { INotebookManager } from 'sql/services/notebook/notebookService';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
@@ -44,6 +44,9 @@ export class NotebookModelStub implements INotebookModel {
|
||||
get contextsChanged(): Event<void> {
|
||||
throw new Error('method not implemented.');
|
||||
}
|
||||
get contentChanged(): Event<NotebookContentChange> {
|
||||
throw new Error('method not implemented.');
|
||||
}
|
||||
get specs(): nb.IAllKernels {
|
||||
throw new Error('method not implemented.');
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('Cell Model', function (): void {
|
||||
let cellData: nb.ICellContents = {
|
||||
cell_type: CellTypes.Code,
|
||||
source: 'print(\'1\')',
|
||||
metadata: { language: 'python'},
|
||||
metadata: { },
|
||||
execution_count: 1
|
||||
};
|
||||
|
||||
@@ -105,6 +105,22 @@ describe('Cell Model', function (): void {
|
||||
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
|
||||
should(cell.language).equal('scala');
|
||||
});
|
||||
it('Should keep cell language as python if cell has language override', async function (): Promise<void> {
|
||||
let cellData: nb.ICellContents = {
|
||||
cell_type: CellTypes.Code,
|
||||
source: 'print(\'1\')',
|
||||
metadata: { language: 'python'},
|
||||
execution_count: 1
|
||||
};
|
||||
|
||||
let notebookModel = new NotebookModelStub({
|
||||
name: 'scala',
|
||||
version: '',
|
||||
mimetype: ''
|
||||
});
|
||||
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
|
||||
should(cell.language).equal('python');
|
||||
});
|
||||
|
||||
it('Should set cell language to python if no language defined', async function (): Promise<void> {
|
||||
let cellData: nb.ICellContents = {
|
||||
@@ -127,7 +143,7 @@ describe('Cell Model', function (): void {
|
||||
let cellData: nb.ICellContents = {
|
||||
cell_type: CellTypes.Code,
|
||||
source: 'std::cout << "hello world";',
|
||||
metadata: { language: 'python'},
|
||||
metadata: { },
|
||||
execution_count: 1
|
||||
};
|
||||
|
||||
@@ -144,7 +160,7 @@ describe('Cell Model', function (): void {
|
||||
let cellData: nb.ICellContents = {
|
||||
cell_type: CellTypes.Code,
|
||||
source: 'print(\'1\')',
|
||||
metadata: { language: 'python'},
|
||||
metadata: { },
|
||||
execution_count: 1
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export function resolveCommonProperties(commit: string, version: string, machine
|
||||
result['sessionID'] = '';
|
||||
|
||||
// __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
result['commitHash'] = commit;
|
||||
result['commitHash'] = '';
|
||||
// __GDPR__COMMON__ "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
result['version'] = version;
|
||||
// __GDPR__COMMON__ "common.platformVersion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
|
||||
@@ -29,7 +29,8 @@ import { coalesce } from 'vs/base/common/arrays';
|
||||
import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IEditorGroupView, IEditorOpeningEvent, EditorGroupsServiceImpl, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
|
||||
import { convertEditorInput } from 'sql/parts/common/customInputConverter';
|
||||
//{{ SQL CARBON EDIT }}
|
||||
import { convertEditorInput, getFileMode } from 'sql/parts/common/customInputConverter';
|
||||
|
||||
type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput;
|
||||
|
||||
@@ -67,7 +68,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IUriDisplayService private uriDisplayService: IUriDisplayService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IConfigurationService private configurationService: IConfigurationService
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -495,9 +496,11 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
const untitledInput = <IUntitledResourceInput>input;
|
||||
if (!untitledInput.resource || typeof untitledInput.filePath === 'string' || (untitledInput.resource instanceof URI && untitledInput.resource.scheme === Schemas.untitled)) {
|
||||
// {{SQL CARBON EDIT}}
|
||||
|
||||
let mode: string = getFileMode( this.instantiationService, untitledInput.resource);
|
||||
return convertEditorInput(this.untitledEditorService.createOrGet(
|
||||
untitledInput.filePath ? URI.file(untitledInput.filePath) : untitledInput.resource,
|
||||
'sql',
|
||||
mode,
|
||||
untitledInput.contents,
|
||||
untitledInput.encoding
|
||||
), undefined, this.instantiationService);
|
||||
|
||||
Reference in New Issue
Block a user