- Show mongo as an explorer view

- Implement connect to database feature
- Implement new scrapbook feature
- Remove the linking between scrapbook and explorer
- Create an untitled file for result
This commit is contained in:
Sandeep Somavarapu 2017-05-05 12:43:36 +02:00
Родитель 4696ee6826
Коммит 6058bf0705
10 изменённых файлов: 201 добавлений и 188 удалений

Просмотреть файл

@ -25,13 +25,6 @@
],
"main": "./out/src/extension",
"contributes": {
"views": [
{
"id": "mongoExplorer",
"label": "Mongo",
"icon": "resources/icons/database.png"
}
],
"languages": [
{
"id": "mongo",
@ -54,20 +47,11 @@
}
],
"commands": [
{
"command": "mongo.resource.onClick",
"title": "Open Mongo Shell Editor"
},
{
"category": "Mongo",
"command": "mongo.execute",
"title": "Execute Script"
},
{
"category": "Mongo",
"command": "mongo.executeLine",
"title": "Execute Script at cursor"
},
{
"category": "Mongo",
"command": "mongo.addServer",
@ -81,6 +65,34 @@
"category": "Mongo",
"command": "mongo.removeServer",
"title": "Remove Server"
},
{
"category": "Mongo",
"command": "mongo.connect",
"title": "Connect"
},
{
"category": "Mongo",
"command": "mongo.connectDb",
"title": "Connect Database"
},
{
"category": "Mongo",
"command": "mongo.newScrapbook",
"title": "New Scrapbook",
"icon": {
"light": "resources/icons/light/addFile.svg",
"dark": "resources/icons/dark/addFile.svg"
}
},
{
"category": "Mongo",
"command": "mongo.refreshExplorer",
"title": "Refresh",
"icon": {
"light": "resources/icons/light/refresh.svg",
"dark": "resources/icons/dark/refresh.svg"
}
}
],
"menus": {
@ -88,35 +100,49 @@
{
"command": "mongo.execute",
"when": "resourceLangId==mongo"
},
{
"command": "mongo.executeLine",
"when": "resourceLangId==mongo"
}
],
"view/title": [
{
"command": "mongo.refreshExplorer",
"when": "view == mongoExplorer",
"group": "navigation"
},
{
"command": "mongo.addServer",
"when": "view == mongoExplorer",
"group": "navigation"
},
{
"command": "mongo.newScrapbook",
"when": "view == mongoExplorer",
"group": "navigation"
}
],
"view/resource": [
{
"command": "mongo.removeServer",
"when": "view == mongoExplorer",
"when": "view == mongoExplorer && resource == mongoServer",
"group": "navigation"
},
{
"command": "mongo.connect",
"when": "view == mongoExplorer && resource == mongoDb",
"group": "navigation"
}
],
"commandPalette": [
{
"command": "mongo.connect",
"when": "view == mongoExplorer && resource == mongoDb"
}
]
},
"keybindings": [
{
"command": "mongo.execute",
"key": "shift+f8"
},
{
"command": "mongo.executeLine",
"key": "f8"
"key": "cmd+`",
"when": "resourceLangId==mongo"
}
],
"configuration": {

Просмотреть файл

@ -0,0 +1,3 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><polygon fill="#C5C5C5" points="12,3 8,3 8,4 11,4 11,7 14,7 14,14 6,14 6,8 5,8 5,15 15,15 15,6"/><path fill="#89D185" d="M7 3.018h-2v-2.018h-1.981v2.018h-2.019v1.982h2.019v2h1.981v-2h2v-1.982z"/></svg>

После

Ширина:  |  Высота:  |  Размер: 419 B

Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#2D2D30"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#C5C5C5"/></svg>

После

Ширина:  |  Высота:  |  Размер: 986 B

Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><polygon points="13,2 8,2 6,2 6,0 2,0 2,2 0,2 0,6 2,6 2,8 4,8 4,16 16,16 16,5" fill="#F6F6F6"/><polygon points="12,3 8,3 8,4 11,4 11,7 14,7 14,14 6,14 6,8 5,8 5,15 15,15 15,6" fill="#424242"/><path d="M7 3.018h-2v-2.018h-1.981v2.018h-2.019v1.982h2.019v2h1.981v-2h2v-1.982z" fill="#388A34"/><polygon points="11,7 11,4 8,4 8,6 6,6 6,8 6,14 14,14 14,7" fill="#F0EFF1"/></svg>

После

Ширина:  |  Высота:  |  Размер: 435 B

Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#F6F6F6"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#424242"/></svg>

После

Ширина:  |  Высота:  |  Размер: 986 B

Просмотреть файл

@ -2,31 +2,34 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { MongoExplorer } from './mongo/explorer';
import { MongoCommands, ResultDocument } from './mongo/commands';
import { MongoCommands } from './mongo/commands';
import { Model, Database, Server, IMongoResource } from './mongo/mongo';
import MongoDBLanguageClient from './mongo/languageClient';
let connectedDb: Database = null;
let languageClient: MongoDBLanguageClient = null;
let model: Model;
let mongoDocumentCounter = 0;
export function activate(context: vscode.ExtensionContext) {
const languageClient = new MongoDBLanguageClient(context);
// Create the storage folder
if (context.storagePath) {
createStorageFolder(context).then(() => {
const outputChannel = vscode.window.createOutputChannel('Mongo');
const model = new Model({ extensionContext: context, outputChannel });
const resultDocument = new ResultDocument(context);
languageClient = new MongoDBLanguageClient(context);
model = new Model(context.storagePath);
// Mongo Tree View
const treeDataProvider = new MongoExplorer(model);
const treeView = vscode.window.createTreeView('mongoExplorer', treeDataProvider);
context.subscriptions.push(treeView);
const disposable = treeDataProvider.onChange((node) => treeView.refresh(node));
const view = vscode.window.createExplorerView('mongoExplorer', 'Mongo', treeDataProvider);
context.subscriptions.push(view);
const disposable = treeDataProvider.onChange((node) => view.refresh(node));
context.subscriptions.push(new vscode.Disposable(() => disposable.dispose()))
// Commands
context.subscriptions.push(vscode.commands.registerCommand('mongo.addServer', () => { MongoCommands.addServer(model) }));
context.subscriptions.push(vscode.commands.registerCommand('mongo.addServer', () => addServer()));
context.subscriptions.push(vscode.commands.registerCommand('mongo.refreshExplorer', () => view.refresh(model)));
context.subscriptions.push(vscode.commands.registerCommand('mongo.removeServer', (element: IMongoResource) => {
if (element instanceof Server) {
model.remove(element.id);
@ -34,34 +37,62 @@ export function activate(context: vscode.ExtensionContext) {
model.remove(element.server.id);
}
}));
context.subscriptions.push(vscode.commands.registerCommand('mongo.resource.onClick', (element: IMongoResource) => {
vscode.window.setStatusBarMessage('Mongo: Not connected');
context.subscriptions.push(vscode.commands.registerCommand('mongo.connect', (element: Database) => connectToDatabase(element)));
context.subscriptions.push(vscode.commands.registerCommand('mongo.connectDb', () => {
vscode.window.showQuickPick(getDatabaseQuickPicks()).then(pick => connectToDatabase(pick.database));
}));
context.subscriptions.push(vscode.commands.registerCommand('mongo.newScrapbook', (element: IMongoResource) => {
if (element instanceof Database) {
languageClient.connect(element);
MongoCommands.openShell(element);
connectToDatabase(element);
}
let uri = vscode.Uri.file(path.join(vscode.workspace.rootPath, `Scrapbook-${++mongoDocumentCounter}.mongo`));
uri = uri.with({ scheme: 'untitled' });
vscode.workspace.openTextDocument(uri)
.then(textDocument => vscode.window.showTextDocument(textDocument));
}));
context.subscriptions.push(vscode.commands.registerCommand('mongo.execute', () => MongoCommands.executeScript(model, resultDocument, outputChannel, true)));
context.subscriptions.push(vscode.commands.registerCommand('mongo.executeLine', () => MongoCommands.executeScript(model, resultDocument, outputChannel, false)));
context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor((editor => {
if (editor) {
const database = MongoCommands.getDatabaseForDocument(editor.document, model);
if (database) {
vscode.window.setStatusBarMessage('Connected to ' + database.id);
}
}
})));
context.subscriptions.push(vscode.commands.registerCommand('mongo.execute', () => MongoCommands.executeScript(connectedDb)));
});
} else {
context.subscriptions.push(vscode.window.createTreeView('mongoExplorer', {
provideRootNode(): any {
vscode.window.showInformationMessage('Open a workspace first.')
return {};
},
}));
}
}
function addServer(): void {
vscode.window.showInputBox({
placeHolder: 'mongodb://host:port'
}).then(value => {
if (value) {
model.add(value);
}
});
}
class DatabaseQuickPick implements vscode.QuickPickItem {
readonly label: string;
readonly description: string;
constructor(readonly database: Database) {
this.label = database.label;
this.description = database.server.label + '/' + database.label;
}
}
function getDatabaseQuickPicks(): Thenable<DatabaseQuickPick[]> {
const quickPicks: DatabaseQuickPick[] = [];
return model.getChildren().then(servers => {
return Promise.all(servers.map(server => server.getChildren()))
.then(allDatabases => {
allDatabases.forEach(databases => {
quickPicks.push(...databases.map(database => new DatabaseQuickPick(<Database>database)));
});
return quickPicks;
})
});
}
function connectToDatabase(database: Database): void {
connectedDb = database;
languageClient.connect(database);
vscode.window.setStatusBarMessage('Mongo: Connected to ' + database.server.host + '/' + connectedDb.id);
}
async function createStorageFolder(context: vscode.ExtensionContext): Promise<void> {

Просмотреть файл

@ -3,103 +3,35 @@ import * as vscode from 'vscode';
import { Model, Server, Database } from './mongo';
import * as fs from 'fs';
export class ResultDocument implements vscode.TextDocumentContentProvider {
private _result: Map<string, string> = new Map<string, any>();
private _onDidChange: vscode.EventEmitter<vscode.Uri> = new vscode.EventEmitter<vscode.Uri>();
public readonly onDidChange: vscode.Event<vscode.Uri> = this._onDidChange.event;
constructor(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('mongo', this));
}
provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult<string> {
const result = this._result.get(uri.toString())
return result ? result : '';
}
setResult(uri: vscode.Uri, result: string) {
this._result.set(uri.toString(), result);
this._onDidChange.fire(uri);
}
}
export class MongoCommands {
public static addServer(model: Model): void {
vscode.window.showInputBox({
placeHolder: 'mongodb://host:port'
}).then(value => {
if (value) {
model.add(value);
}
});
public static executeScript(database: Database): void {
if (!database) {
vscode.window.showErrorMessage('Please connect to the database first');
return;
}
public static openShell(database: Database): void {
let uri = vscode.Uri.file(path.join(vscode.workspace.rootPath, database.server.host + '_' + database.server.port + '_' + database.label + '.mongo'));
const exists = fs.existsSync(uri.fsPath);
if (!exists) {
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor.document.languageId !== 'mongo') {
return;
}
const text = activeEditor.document.lineAt(activeEditor.selection.start.line).text;
let uri = vscode.Uri.file(path.join(vscode.workspace.rootPath, 'result.json'));
uri = uri.with({ scheme: 'untitled' });
}
vscode.workspace.openTextDocument(uri)
.then(textDocument => {
return vscode.window.showTextDocument(textDocument)
.then(textDocument => vscode.window.showTextDocument(textDocument, vscode.ViewColumn.Two, true))
.then(editor => {
if (!exists && !MongoCommands.isShellDocument(textDocument, database)) {
editor.edit(builder => {
builder.insert(new vscode.Position(0, 0), database.connectionString + '\n');
});
}
});
});
}
public static executeScript(model: Model, resultDocument: ResultDocument, outputChannel: vscode.OutputChannel, selection: boolean): void {
const editor = vscode.window.activeTextEditor;
if (editor.document.languageId === 'mongo' || editor.document.uri.fsPath.endsWith('.mongo')) {
const text = selection ? MongoCommands.getSelectedText(editor) : editor.document.lineAt(editor.selection.start.line).text;
const database = MongoCommands.getDatabaseForDocument(editor.document, model);
if (database) {
database.executeScript(text)
.then(result => {
const uri = vscode.Uri.parse('mongo://test/result.json');
resultDocument.setResult(uri, result);
vscode.workspace.openTextDocument(uri)
.then(textDocument => {
vscode.window.showTextDocument(textDocument, vscode.ViewColumn.Two, true);
editor.edit(editorBuilder => {
if (editor.document.lineCount > 0) {
const lastLine = editor.document.lineAt(editor.document.lineCount - 1);
editorBuilder.delete(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(lastLine.range.start.line, lastLine.range.end.character)));
}
editorBuilder.insert(new vscode.Position(0, 0), result);
});
});
outputChannel.append(result);
});
} else {
vscode.window.showErrorMessage('Please connect to the database first');
}
}
}
private static getSelectedText(editor: vscode.TextEditor): string {
const selection = editor.selection;
if (selection.start.isEqual(selection.end)) {
editor.document.getText();
}
return editor.document.getText(selection);
}
public static getDatabaseForDocument(document: vscode.TextDocument, model: Model) {
if (document.languageId === 'mongo' || document.uri.fsPath.endsWith('.mongo')) {
for (const server of model.servers) {
for (const database of server.databases) {
if (MongoCommands.isShellDocument(document, database)) {
return database;
}
}
}
}
return null;
}
public static isShellDocument(document: vscode.TextDocument, database: Database): boolean {
return database.connectionString === document.lineAt(0).text;
}
}

Просмотреть файл

@ -32,6 +32,10 @@ export class MongoExplorer implements TreeDataProvider<IMongoResource> {
};
}
getContextKey(node: IMongoResource): string {
return node.contextKey;
}
resolveChildren(node: IMongoResource): Thenable<IMongoResource[]> {
const disposables = this._disposables.get(node);
if (disposables) {

Просмотреть файл

@ -1,28 +1,23 @@
import * as vscode from 'vscode';
import * as cp from 'child_process';
import * as fs from 'fs';
import { MongoClient, Db, ReadPreference, Code, Server as MongoServer } from 'mongodb';
import { MongoClient, Db, ReadPreference, Code, Server as MongoServer, Collection as MongoCollection } from 'mongodb';
import { Shell } from './shell';
import { EventEmitter, Event, Command } from 'vscode';
export interface IMongoContext {
extensionContext: vscode.ExtensionContext;
outputChannel: vscode.OutputChannel;
}
export interface IMongoResource {
label: string;
type: string;
getChildren?(): Thenable<IMongoResource[]>;
onChange?: Event<void>
contextKey?: string;
}
class ServersJson {
private _filePath: string;
constructor(context: vscode.ExtensionContext) {
this._filePath = context.storagePath + '/servers.json';
constructor(storagePath: string) {
this._filePath = storagePath + '/servers.json';
}
async load(): Promise<string[]> {
@ -66,8 +61,8 @@ export class Model implements IMongoResource {
private _onChange: EventEmitter<void> = new EventEmitter<void>();
readonly onChange: Event<void> = this._onChange.event;
constructor(private context: IMongoContext) {
this._serversJson = new ServersJson(context.extensionContext);
constructor(storagePath: string) {
this._serversJson = new ServersJson(storagePath);
}
getChildren(): Promise<IMongoResource[]> {
@ -105,18 +100,18 @@ export class Model implements IMongoResource {
private resolveServer(connectionString: string): Promise<Server> {
return <Promise<Server>>MongoClient.connect(connectionString)
.then(db => {
return new Server(connectionString, db.serverConfig, this.context);
return new Server(connectionString, db.serverConfig);
});
}
}
export class Server implements IMongoResource {
readonly type: string = 'mongoServer';
readonly contextKey: string = 'mongoServer';
private _databases: Database[] = [];
constructor(public readonly id: string, private readonly mongoServer: MongoServer, private context: IMongoContext) {
constructor(public readonly id: string, private readonly mongoServer: MongoServer) {
}
get host(): string {
@ -137,7 +132,7 @@ export class Server implements IMongoResource {
return <Promise<IMongoResource[]>>MongoClient.connect(this.id)
.then(db => db.admin().listDatabases()
.then((value: { databases: { name }[] }) => {
this._databases = value.databases.map(database => new Database(database.name, this, this.context));
this._databases = value.databases.map(database => new Database(database.name, this));
db.close();
return <IMongoResource[]>this._databases;
}));
@ -150,19 +145,26 @@ export class Server implements IMongoResource {
export class Database implements IMongoResource {
readonly type: string = 'mongoDb';
readonly connectionString: string;
readonly contextKey: string = 'mongoDb';
private shell: Shell;
constructor(readonly id: string, readonly server: Server, private context: IMongoContext) {
this.connectionString = '//connection:' + this.server.id + '/' + this.id;
constructor(readonly id: string, readonly server: Server) {
}
get label(): string {
return this.id;
}
readonly canHaveChildren: boolean = false;
readonly canHaveChildren: boolean = true;
getChildren(): Promise<IMongoResource[]> {
return <Promise<IMongoResource[]>>this.getDb().then(db => {
return db.collections().then(collections => {
return collections.map(collection => new Collection(collection));
})
});
}
getDb(): Promise<Db> {
const uri = vscode.Uri.parse(this.server.id);
@ -224,3 +226,15 @@ export class Database implements IMongoResource {
});
}
}
export class Collection implements IMongoResource {
constructor(private collection: MongoCollection) {
}
get label(): string {
return this.collection.collectionName;
}
readonly canHaveChildren: boolean = false;
}

32
src/typings/vscode.proposed.d.ts поставляемый
Просмотреть файл

@ -524,34 +524,26 @@ declare module 'vscode' {
export namespace window {
/**
* Show window-wide progress, e.g. in the status bar, for the provided task. The task is
* considering running as long as the promise it returned isn't resolved or rejected.
*
* @param task A function callback that represents a long running operation.
*/
export function withWindowProgress<R>(title: string, task: (progress: Progress<string>, token: CancellationToken) => Thenable<R>): Thenable<R>;
export function sampleFunction(): Thenable<any>;
}
export namespace window {
/**
* Create a new [TreeView](#TreeView) instance.
* Create a new explorer view.
*
* @param viewId A unique id that identifies the view.
* @param provider A [TreeDataProvider](#TreeDataProvider).
* @return An instance of [TreeView](#TreeView).
* @param id View id.
* @param name View name.
* @param dataProvider A [TreeDataProvider](#TreeDataProvider).
* @return An instance of [View](#View).
*/
export function createTreeView<T>(viewId: string, provider: TreeDataProvider<T>): TreeView<T>;
export function createExplorerView<T>(id: string, name: string, dataProvider: TreeDataProvider<T>): View<T>;
}
/**
* An source control is able to provide [resource states](#SourceControlResourceState)
* to the editor and interact with the editor in several source control related ways.
* A view to interact with nodes
*/
export interface TreeView<T> {
export interface View<T> {
/**
* Refresh the given nodes
@ -610,6 +602,14 @@ declare module 'vscode' {
*/
getHasChildren?(node: T): boolean;
/**
* Provider a context key to be set for the node. This can be used to describe actions for each node.
*
* @param node The node from which the provider computes context key.
* @return A context key.
*/
getContextKey?(node: T): string;
/**
* Get the command to execute when `node` is clicked.
*