Feature/copy paste (#83)
* initial copy paste code * initial copy-paste pipe * added copy paste functionality * updated package.json * added tests; removed console logs * removed unnecessary code
This commit is contained in:
Родитель
9428664c43
Коммит
35cd4a881b
14
package.json
14
package.json
|
@ -56,20 +56,22 @@
|
|||
"vscode": "^0.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.15.2",
|
||||
"copy-paste": "^1.3.0",
|
||||
"decompress": "^4.0.0",
|
||||
"ejs": "^2.4.2",
|
||||
"error-ex": "^1.3.0",
|
||||
"express": "^4.13.3",
|
||||
"figures": "^1.4.0",
|
||||
"getmac": "1.2.1",
|
||||
"request": "^2.73.0",
|
||||
"underscore": "^1.8.3",
|
||||
"vscode-extension-telemetry": "^0.0.5",
|
||||
"vscode-languageclient": "^2.0.0",
|
||||
"fs-extra-promise": "^0.3.1",
|
||||
"getmac": "1.2.1",
|
||||
"http-proxy-agent": "^1.0.0",
|
||||
"https-proxy-agent": "^1.0.0",
|
||||
"request": "^2.73.0",
|
||||
"tmp": "^0.0.28",
|
||||
"decompress": "^4.0.0"
|
||||
"underscore": "^1.8.3",
|
||||
"vscode-extension-telemetry": "^0.0.5",
|
||||
"vscode-languageclient": "^2.0.0"
|
||||
},
|
||||
"contributes": {
|
||||
"languages": [
|
||||
|
|
|
@ -3,6 +3,7 @@ import path = require('path');
|
|||
import Utils = require('../models/utils');
|
||||
import Constants = require('../models/constants');
|
||||
import Interfaces = require('../models/interfaces');
|
||||
const bodyParser = require('body-parser');
|
||||
const express = require('express');
|
||||
|
||||
export default class LocalWebService {
|
||||
|
@ -17,6 +18,7 @@ export default class LocalWebService {
|
|||
LocalWebService._vscodeExtensionPath = extensionPath;
|
||||
LocalWebService._staticContentPath = path.join(extensionPath, LocalWebService._htmlContentLocation);
|
||||
this.app.use(express.static(LocalWebService.staticContentPath));
|
||||
this.app.use( bodyParser.json() );
|
||||
this.app.set('view engine', 'ejs');
|
||||
Utils.logDebug(Constants.msgLocalWebserviceStaticContent + LocalWebService.staticContentPath);
|
||||
}
|
||||
|
@ -42,6 +44,11 @@ export default class LocalWebService {
|
|||
this.app.get(segment, handler);
|
||||
}
|
||||
|
||||
addPostHandler(type: Interfaces.ContentType, handler: (req, res) => void): void {
|
||||
let segment = '/' + Interfaces.ContentTypes[type];
|
||||
this.app.post(segment, handler);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
const port = this.app.listen(0).address().port; // 0 = listen on a random port
|
||||
Utils.logDebug(Constants.msgLocalWebserviceStarted + port);
|
||||
|
|
|
@ -9,6 +9,10 @@ import { BatchSummary, QueryExecuteParams, QueryExecuteRequest,
|
|||
QueryExecuteCompleteNotificationResult, QueryExecuteSubsetResult,
|
||||
QueryExecuteSubsetParams, QueryDisposeParams, QueryExecuteSubsetRequest,
|
||||
QueryDisposeRequest } from '../models/contracts/queryExecute';
|
||||
import { ISlickRange } from '../models/interfaces';
|
||||
|
||||
|
||||
const ncp = require('copy-paste');
|
||||
|
||||
export interface IResultSet {
|
||||
columns: string[];
|
||||
|
@ -165,4 +169,36 @@ export default class QueryRunner {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
public copyResults(selection: ISlickRange[], batchId: number, resultId: number): Promise<void> {
|
||||
const self = this;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let copyString = '';
|
||||
// create a mapping of the ranges to get promises
|
||||
let tasks = selection.map((range, i) => {
|
||||
return () => {
|
||||
return self.getRows(range.fromRow, range.toRow - range.fromRow + 1, batchId, resultId).then((result) => {
|
||||
// iterate over the rows to paste into the copy string
|
||||
for (let row of result.resultSubset.rows) {
|
||||
// iterate over the cells we want from that row
|
||||
for (let cell = range.fromCell; cell <= range.toCell; cell++) {
|
||||
copyString += row[cell] + '\t';
|
||||
}
|
||||
copyString += '\r\n';
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
let p = tasks[0]();
|
||||
for (let i = 1; 1 < tasks.length; i++) {
|
||||
p = p.then(tasks[i]);
|
||||
}
|
||||
p.then(() => {
|
||||
ncp.copy(copyString, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ export const outputContentTypeResultsetMeta = 'resultsetsMeta';
|
|||
export const outputContentTypeColumns = 'columns';
|
||||
export const outputContentTypeRows = 'rows';
|
||||
export const outputContentTypeSaveResults = 'saveResults';
|
||||
export const outputContentTypeCopy = 'copyResults';
|
||||
export const outputServiceLocalhost = 'http://localhost:';
|
||||
export const msgContentProviderSqlOutputHtml = 'sqlOutput.ejs';
|
||||
|
||||
|
|
|
@ -9,9 +9,17 @@ export enum ContentType {
|
|||
ResultsetsMeta = 2,
|
||||
Columns = 3,
|
||||
Rows = 4,
|
||||
SaveResults = 5
|
||||
SaveResults = 5,
|
||||
Copy = 6
|
||||
};
|
||||
|
||||
export interface ISlickRange {
|
||||
fromCell: number;
|
||||
fromRow: number;
|
||||
toCell: number;
|
||||
toRow: number;
|
||||
}
|
||||
|
||||
export enum AuthenticationTypes {
|
||||
Integrated = 1,
|
||||
SqlLogin = 2,
|
||||
|
@ -24,7 +32,8 @@ export const ContentTypes = [
|
|||
Constants.outputContentTypeResultsetMeta,
|
||||
Constants.outputContentTypeColumns,
|
||||
Constants.outputContentTypeRows,
|
||||
Constants.outputContentTypeSaveResults
|
||||
Constants.outputContentTypeSaveResults,
|
||||
Constants.outputContentTypeCopy
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -103,6 +103,17 @@ export class SqlOutputContentProvider implements vscode.TextDocumentContentProvi
|
|||
res.send();
|
||||
});
|
||||
|
||||
this._service.addPostHandler(Interfaces.ContentType.Copy, function(req, res): void {
|
||||
let uri = decodeURI(req.query.uri);
|
||||
let resultId = req.query.resultId;
|
||||
let batchId = req.query.batchId;
|
||||
let selection: Interfaces.ISlickRange[] = req.body;
|
||||
self._queryResultsMap.get(uri).copyResults(selection, batchId, resultId).then(() => {
|
||||
res.status = 200;
|
||||
res.send();
|
||||
});
|
||||
});
|
||||
|
||||
// start express server on localhost and listen on a random port
|
||||
try {
|
||||
this._service.start();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
import {Component, OnInit, Inject, forwardRef, ViewChild} from '@angular/core';
|
||||
import {Component, OnInit, Inject, forwardRef, ViewChild, ViewChildren, QueryList} from '@angular/core';
|
||||
import {IColumnDefinition} from './slickgrid/ModelInterfaces';
|
||||
import {IObservableCollection} from './slickgrid/BaseLibrary';
|
||||
import {IGridDataRow} from './slickgrid/SharedControlInterfaces';
|
||||
|
@ -48,8 +48,10 @@ export class AppComponent implements OnInit {
|
|||
private messages: string[] = [];
|
||||
private selected: SelectedTab;
|
||||
private windowSize = 50;
|
||||
private c_key = 67;
|
||||
public SelectedTab = SelectedTab;
|
||||
@ViewChild(ContextMenu) contextMenu: ContextMenu;
|
||||
@ViewChildren(SlickGrid) slickgrids: QueryList<SlickGrid>;
|
||||
|
||||
constructor(@Inject(forwardRef(() => DataService)) private dataService: DataService) {}
|
||||
|
||||
|
@ -170,4 +172,14 @@ export class AppComponent implements OnInit {
|
|||
tabChange(to: SelectedTab): void {
|
||||
this.selected = to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keyboard events on angular, currently only needed for copy-paste
|
||||
*/
|
||||
onKey(e: any, batchId: number, resultId: number, index: number): void {
|
||||
if ((e.ctrlKey || e.metaKey) && e.which === this.c_key) {
|
||||
let selection = this.slickgrids.toArray()[index].getSelectedRanges();
|
||||
this.dataService.copyResults(selection, batchId, resultId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<div class="fullsize box">
|
||||
<tabs class="boxRow content box" [selected]="selected" (tabChange)="tabChange($event)">
|
||||
<tab [id]="SelectedTab.Results" tabTitle="Results" [show]="dataSets.length > 0" class="boxRow content box">
|
||||
<slick-grid *ngFor="let dataSet of dataSets"
|
||||
<slick-grid *ngFor="let dataSet of dataSets; let i = index"
|
||||
[columnDefinitions]="dataSet.columnDefinitions"
|
||||
[dataRows]="dataSet.dataRows"
|
||||
(contextMenu)="openContextMenu($event, dataSet.batchId, dataSet.resultId)"
|
||||
showDataTypeIcon="false"
|
||||
showHeader="true"
|
||||
(keydown)="onKey($event, dataSet.batchId, dataSet.resultId, i)"
|
||||
class="boxRow content box slickgrid">
|
||||
</slick-grid>
|
||||
<context-menu (clickEvent)="handleContextClick($event)"></context-menu>
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
import {Injectable, Inject, forwardRef} from '@angular/core';
|
||||
import {Http} from '@angular/http';
|
||||
import {Http, Headers} from '@angular/http';
|
||||
import {Observable} from 'rxjs/Rx';
|
||||
import { IDbColumn, ResultSetSubset, IGridBatchMetaData } from './../interfaces';
|
||||
import {ISlickRange} from './SlickGrid/SelectionModel';
|
||||
|
||||
/**
|
||||
* Service which performs the http requests to get the data resultsets from the server.
|
||||
|
@ -213,4 +214,17 @@ export class DataService {
|
|||
self.http.get('/saveResults?'
|
||||
+ '&uri=' + self.uri + '&format=' + format + '&batchIndex=' + batchIndex + '&resultSetNo=' + resultSetNumber).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a copy request
|
||||
* @param selection The selection range to copy
|
||||
* @param batchId The batch id of the result to copy from
|
||||
* @param resultId The result id of the result to copy from
|
||||
*/
|
||||
copyResults(selection: ISlickRange[], batchId: number, resultId: number): void {
|
||||
const self = this;
|
||||
let headers = new Headers();
|
||||
let url = '/copyResults?' + '&uri=' + self.uri + '&batchId=' + batchId + '&resultId=' + resultId;
|
||||
self.http.post(url, selection, { headers: headers }).subscribe();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
});
|
||||
|
||||
function DragRowSelectionModel(options) {
|
||||
const left_arrow = 37, up_arrow = 38, right_arrow = 39, down_arrow = 40, a_key = 65;
|
||||
const left_arrow = 37, up_arrow = 38, right_arrow = 39, down_arrow = 40, a_key = 65, c_key = 67;
|
||||
var _grid;
|
||||
var _dragStart;
|
||||
var _dragRow;
|
||||
|
@ -106,7 +106,7 @@
|
|||
_ranges = [new Slick.Range(0, 0, _grid.getDataLength() - 1, _grid.getColumns().length)]
|
||||
setSelectedRanges(_ranges);
|
||||
e.preventDefault();
|
||||
- e.stopPropagation();
|
||||
e.stopPropagation();
|
||||
}
|
||||
// do we have a context to navigate on
|
||||
else if (activeCell) {
|
||||
|
@ -127,7 +127,7 @@
|
|||
_grid.setActiveCell(activeCell.row, activeCell.cell - 1);
|
||||
setSelectedRanges(_ranges);
|
||||
e.preventDefault();
|
||||
- e.stopPropagation();
|
||||
e.stopPropagation();
|
||||
// up arrow
|
||||
} else if (e.which == up_arrow && activeCell.row > 0) {
|
||||
if (e.shiftKey) {
|
||||
|
@ -143,7 +143,7 @@
|
|||
_grid.setActiveCell(activeCell.row - 1, activeCell.cell);
|
||||
setSelectedRanges(_ranges);
|
||||
e.preventDefault();
|
||||
- e.stopPropagation();
|
||||
e.stopPropagation();
|
||||
// right arrow
|
||||
} else if (e.which == right_arrow && activeCell.cell < _grid.getColumns().length) {
|
||||
if (e.shiftKey) {
|
||||
|
@ -159,7 +159,7 @@
|
|||
_grid.setActiveCell(activeCell.row, activeCell.cell + 1);
|
||||
setSelectedRanges(_ranges);
|
||||
e.preventDefault();
|
||||
- e.stopPropagation();
|
||||
e.stopPropagation();
|
||||
// down arrow
|
||||
} else if (e.which == down_arrow && activeCell.row < _grid.getDataLength()) {
|
||||
if (e.shiftKey) {
|
||||
|
@ -175,7 +175,7 @@
|
|||
_grid.setActiveCell(activeCell.row + 1, activeCell.cell);
|
||||
setSelectedRanges(_ranges);
|
||||
e.preventDefault();
|
||||
- e.stopPropagation();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ export class SelectionModel implements ISlickSelectionModel {
|
|||
this.updateSelectedRanges(ranges);
|
||||
}
|
||||
|
||||
getSelectedRanges(): ISlickRange[] {
|
||||
return this._rowSelectionModel.getSelectedRanges();
|
||||
}
|
||||
|
||||
changeSelectedRanges(selections: ISelectionRange[]): void {
|
||||
let slickRange = (selections || []).map(s =>
|
||||
this._slickRangeFactory(s.startRow, s.startColumn, s.endRow - 1, s.endColumn - 1)
|
||||
|
@ -173,6 +177,7 @@ export interface ISlickSelectionModel {
|
|||
init(grid: any): void;
|
||||
destroy(): void;
|
||||
setSelectedRanges(ranges: ISlickRange[]): void;
|
||||
getSelectedRanges(): ISlickRange[];
|
||||
}
|
||||
|
||||
export interface ISlickEventHandler {
|
||||
|
|
|
@ -13,6 +13,7 @@ import {IGridDataRow} from './SharedControlInterfaces';
|
|||
import {IColumnDefinition} from './ModelInterfaces';
|
||||
import {LocalizationService} from './LocalizationService';
|
||||
import {GridSyncService} from './GridSyncService';
|
||||
import {ISlickRange} from './SelectionModel';
|
||||
|
||||
enum FieldType {
|
||||
String = 0,
|
||||
|
@ -319,6 +320,10 @@ export class SlickGrid implements OnChanges, OnInit, OnDestroy, DoCheck {
|
|||
}
|
||||
}
|
||||
|
||||
public getSelectedRanges(): ISlickRange[] {
|
||||
return this._gridSyncService.selectionModel.getSelectedRanges();
|
||||
}
|
||||
|
||||
/* tslint:disable:member-ordering */
|
||||
private getColumnEditor = (column: any): any => {
|
||||
let columnId = column.id;
|
||||
|
|
|
@ -7,6 +7,9 @@ import SqlToolsServerClient from './../src/languageservice/serviceclient';
|
|||
import { QueryExecuteParams } from './../src/models/contracts/queryExecute';
|
||||
import VscodeWrapper from './../src/controllers/vscodeWrapper';
|
||||
import StatusView from './../src/views/statusView';
|
||||
import { ISlickRange } from './../src/models/interfaces';
|
||||
|
||||
const ncp = require('copy-paste');
|
||||
|
||||
suite('Query Runner tests', () => {
|
||||
|
||||
|
@ -169,4 +172,47 @@ suite('Query Runner tests', () => {
|
|||
testVscodeWrapper.verify(x => x.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
||||
});
|
||||
});
|
||||
|
||||
test('Correctly copy pastes a selection', () => {
|
||||
const TAB = '\t';
|
||||
const CLRF = '\r\n';
|
||||
const finalString = '1' + TAB + '2' + TAB + CLRF +
|
||||
'3' + TAB + '4' + TAB + CLRF +
|
||||
'5' + TAB + '6' + TAB + CLRF +
|
||||
'7' + TAB + '8' + TAB + CLRF +
|
||||
'9' + TAB + '10' + TAB + CLRF;
|
||||
let testuri = 'test';
|
||||
let testresult = {
|
||||
message: '',
|
||||
resultSubset: {
|
||||
rowCount: 5,
|
||||
rows: [
|
||||
['1', '2'],
|
||||
['3', '4'],
|
||||
['5', '6'],
|
||||
['7', '8'],
|
||||
['9', '10']
|
||||
]
|
||||
}
|
||||
};
|
||||
let testRange: ISlickRange[] = [{fromCell: 0, fromRow: 0, toCell: 1, toRow: 4}];
|
||||
testSqlToolsServerClient.setup(x => x.sendRequest(TypeMoq.It.isAny(),
|
||||
TypeMoq.It.isAny())).callback(() => {
|
||||
// testing
|
||||
}).returns(() => { return Promise.resolve(testresult); });
|
||||
testStatusView.setup(x => x.executingQuery(TypeMoq.It.isAnyString()));
|
||||
let queryRunner = new QueryRunner(
|
||||
undefined,
|
||||
testStatusView.object,
|
||||
testSqlOutputContentProvider.object,
|
||||
testSqlToolsServerClient.object,
|
||||
testQueryNotificationHandler.object,
|
||||
testVscodeWrapper.object
|
||||
);
|
||||
queryRunner.uri = testuri;
|
||||
return queryRunner.copyResults(testRange, 0, 0).then(() => {
|
||||
let pasteContents = ncp.paste();
|
||||
assert.equal(pasteContents, finalString);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче