Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1 Enhancements/6350.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Change copy back to code button in the interactive window to insert wherever the current selection is.
54 changes: 38 additions & 16 deletions src/client/datascience/interactive-window/interactiveWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import '../../common/extensions';
import { nbformat } from '@jupyterlab/coreutils';
import * as fs from 'fs-extra';
import { inject, injectable, multiInject } from 'inversify';
import * as os from 'os';
import * as path from 'path';
import * as uuid from 'uuid/v4';
import { ConfigurationTarget, Event, EventEmitter, Position, Range, Selection, TextEditor, Uri, ViewColumn } from 'vscode';
Expand All @@ -31,7 +32,7 @@ import * as localize from '../../common/utils/localize';
import { StopWatch } from '../../common/utils/stopWatch';
import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts';
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
import { CellMatcher } from '../cellMatcher';
import { generateCellRanges } from '../cellFactory';
import { EditorContexts, Identifiers, Telemetry } from '../constants';
import { ColumnWarningSize } from '../data-viewing/types';
import { JupyterInstallError } from '../jupyter/jupyterInstallError';
Expand Down Expand Up @@ -464,6 +465,13 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
}
}

@captureTelemetry(Telemetry.CopySourceCode, undefined, false)
public copyCode(args: ICopyCode) {
this.copyCodeInternal(args.source).catch(err => {
this.applicationShell.showErrorMessage(err);
});
}

protected async activating() {
// Only activate if the active editor is empty. This means that
// vscode thinks we are actually supposed to have focus. It would be
Expand Down Expand Up @@ -927,34 +935,48 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
}
}

@captureTelemetry(Telemetry.CopySourceCode, undefined, false)
private copyCode(args: ICopyCode) {
this.copyCodeInternal(args.source).catch(err => {
this.applicationShell.showErrorMessage(err);
});
}

private async copyCodeInternal(source: string) {
let editor = this.documentManager.activeTextEditor;
if (!editor || editor.document.languageId !== PYTHON_LANGUAGE) {
// Find the first visible python editor
const pythonEditors = this.documentManager.visibleTextEditors.filter(
e => e.document.languageId === PYTHON_LANGUAGE);
e => e.document.languageId === PYTHON_LANGUAGE || e.document.isUntitled);

if (pythonEditors.length > 0) {
editor = pythonEditors[0];
}
}
if (editor && editor.document.languageId === PYTHON_LANGUAGE) {
const cellMatcher = new CellMatcher(this.generateDataScienceExtraSettings());
const hasCellsAlready = cellMatcher.isCell(editor.document.getText());
const line = editor.document.lineCount;
const newCode = hasCellsAlready || line <= 0 ? `\n\n#%%\n${source}` : `\n\n${source}`;
if (editor && (editor.document.languageId === PYTHON_LANGUAGE || editor.document.isUntitled)) {
// Figure out if any cells in this document already.
const ranges = generateCellRanges(editor.document, this.generateDataScienceExtraSettings());
const hasCellsAlready = ranges.length > 0;
Copy link
Copy Markdown
Member

@IanMatthewHuff IanMatthewHuff Jul 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest that this chunk kinda feels like is should be part of CodeWatcher functionality. But I don't think that works for untitled files. #ByDesign

const line = editor.selection.start.line;
const revealLine = line + 1;
let newCode = `${source}${os.EOL}`;
if (hasCellsAlready) {
// See if inside of a range or not.
const matchingRange = ranges.find(r => r.range.start.line <= line && r.range.end.line >= line);

// If in the middle, wrap the new code
if (matchingRange && matchingRange.range.start.line < line && line < editor.document.lineCount - 1) {
newCode = `#%%${os.EOL}${source}${os.EOL}#%%${os.EOL}`;
} else {
newCode = `#%%${os.EOL}${source}${os.EOL}`;
}
} else if (editor.document.lineCount <= 0 || editor.document.isUntitled) {
// No lines in the document at all, just insert new code
newCode = `#%%${os.EOL}${source}${os.EOL}`;
}

await editor.edit((editBuilder) => {
editBuilder.insert(new Position(line, 0), newCode);
});
editor.revealRange(new Range(line + 2, 0, line + source.split('\n').length + 3, 0));
editor.selection = new Selection(new Position(line + 2, 0), new Position(line + 2, 0));
editor.revealRange(new Range(revealLine, 0, revealLine + source.split('\n').length + 3, 0));

// Move selection to just beyond the text we input so that the next
// paste will be right after
const selectionLine = line + newCode.split('\n').length - 1;
editor.selection = new Selection(new Position(selectionLine, 0), new Position(selectionLine, 0));
}
}

Expand Down
44 changes: 25 additions & 19 deletions src/test/datascience/interactiveWindow.functional.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
import * as assert from 'assert';
import * as fs from 'fs-extra';
import { parse } from 'node-html-parser';
import * as os from 'os';
import * as path from 'path';
import * as TypeMoq from 'typemoq';
import { Disposable, TextDocument, TextEditor } from 'vscode';
import { Disposable, Selection, TextDocument, TextEditor } from 'vscode';

import { IApplicationShell, IDocumentManager } from '../../client/common/application/types';
import { PYTHON_LANGUAGE } from '../../client/common/constants';
import { createDeferred } from '../../client/common/utils/async';
import { noop } from '../../client/common/utils/misc';
import { generateCellsFromDocument } from '../../client/datascience/cellFactory';
import { concatMultilineString } from '../../client/datascience/common';
import { EditorContexts } from '../../client/datascience/constants';
import { InteractiveWindowMessageListener } from '../../client/datascience/interactive-window/interactiveWindowMessageListener';
import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow';
import {
InteractiveWindowMessageListener
} from '../../client/datascience/interactive-window/interactiveWindowMessageListener';
import { InteractiveWindowMessages } from '../../client/datascience/interactive-window/interactiveWindowTypes';
import { IInteractiveWindow, IInteractiveWindowProvider } from '../../client/datascience/types';
import { MainPanel } from '../../datascience-ui/history-react/MainPanel';
Expand Down Expand Up @@ -43,9 +46,11 @@ import {
verifyHtmlOnCell,
verifyLastCellInputState
} from './interactiveWindowTestHelpers';
import { MockEditor } from './mockTextEditor';
import { waitForUpdate } from './reactHelpers';

//import { asyncDump } from '../common/asyncDump';
import { MockDocumentManager } from './mockDocumentManager';
// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string
suite('DataScience Interactive Window output tests', () => {
const disposables: Disposable[] = [];
Expand Down Expand Up @@ -469,22 +474,10 @@ for _ in range(50):

runMountedTest('Copy to source input', async (wrapper) => {
const showedEditor = createDeferred();
const textEditors: TextEditor[] = [];
const docManager = TypeMoq.Mock.ofType<IDocumentManager>();
const visibleEditor = TypeMoq.Mock.ofType<TextEditor>();
const dummyDocument = TypeMoq.Mock.ofType<TextDocument>();
dummyDocument.setup(d => d.fileName).returns(() => 'foo.py');
dummyDocument.setup(d => d.languageId).returns(() => PYTHON_LANGUAGE);
dummyDocument.setup(d => d.lineCount).returns(() => 10);
dummyDocument.setup(d => d.getText()).returns(() => '# No cells here');
visibleEditor.setup(v => v.show()).returns(noop);
visibleEditor.setup(v => v.revealRange(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => showedEditor.resolve());
visibleEditor.setup(v => v.document).returns(() => dummyDocument.object);
visibleEditor.setup(v => v.edit(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
textEditors.push(visibleEditor.object);
docManager.setup(a => a.visibleTextEditors).returns(() => textEditors);
docManager.setup(a => a.activeTextEditor).returns(() => undefined);
ioc.serviceManager.rebindInstance<IDocumentManager>(IDocumentManager, docManager.object);
ioc.addDocument('# No cells here', 'foo.py');
const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager;
const editor = await docManager.showTextDocument(docManager.textDocuments[0]) as MockEditor;
editor.setRevealCallback(() => showedEditor.resolve());

// Create an interactive window so that it listens to the results.
const interactiveWindow = await getOrCreateInteractiveWindow();
Expand Down Expand Up @@ -613,4 +606,17 @@ for _ in range(50):
}

}, () => { return ioc; });

runMountedTest('Copy back to source', async (_wrapper) => {
ioc.addDocument(`#%%${os.EOL}print("bar")`, 'foo.py');
const docManager = ioc.get<IDocumentManager>(IDocumentManager);
docManager.showTextDocument(docManager.textDocuments[0]);
const window = await getOrCreateInteractiveWindow() as InteractiveWindow;
window.copyCode({source: 'print("baz")'});
assert.equal(docManager.textDocuments[0].getText(), `#%%${os.EOL}print("baz")${os.EOL}#%%${os.EOL}print("bar")`, 'Text not inserted');
const activeEditor = docManager.activeTextEditor as MockEditor;
activeEditor.selection = new Selection(1, 2, 1, 2);
window.copyCode({source: 'print("baz")'});
assert.equal(docManager.textDocuments[0].getText(), `#%%${os.EOL}#%%${os.EOL}print("baz")${os.EOL}#%%${os.EOL}print("baz")${os.EOL}#%%${os.EOL}print("bar")`, 'Text not inserted');
}, () => { return ioc; });
});
21 changes: 5 additions & 16 deletions src/test/datascience/mockDocumentManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import * as TypeMoq from 'typemoq';
import {
DecorationRenderOptions,
Event,
Expand All @@ -22,18 +21,9 @@ import {

import { IDocumentManager } from '../../client/common/application/types';
import { MockDocument } from './mockDocument';

import { MockEditor } from './mockTextEditor';
// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length

function createTypeMoq<T>(tag: string): TypeMoq.IMock<T> {
// Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class
// is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67
const result = TypeMoq.Mock.ofType<T>();
(result as any).tag = tag;
result.setup((x: any) => x.then).returns(() => undefined);
return result;
}

export class MockDocumentManager implements IDocumentManager {
public textDocuments: TextDocument[] = [];
public activeTextEditor: TextEditor | undefined;
Expand Down Expand Up @@ -77,10 +67,9 @@ export class MockDocumentManager implements IDocumentManager {
public showTextDocument(_document: TextDocument, _column?: ViewColumn, _preserveFocus?: boolean): Thenable<TextEditor>;
public showTextDocument(_document: TextDocument | Uri, _options?: TextDocumentShowOptions): Thenable<TextEditor>;
public showTextDocument(_document: any, _column?: any, _preserveFocus?: any): Thenable<TextEditor> {
const mockEditor = createTypeMoq<TextEditor>('TextEditor');
mockEditor.setup(e => e.document).returns(() => this.lastDocument);
this.activeTextEditor = mockEditor.object;
return Promise.resolve(mockEditor.object);
const mockEditor = new MockEditor(this, this.lastDocument as MockDocument);
this.activeTextEditor = mockEditor;
return Promise.resolve(mockEditor);
}
public openTextDocument(_fileName: string | Uri): Thenable<TextDocument>;
public openTextDocument(_options?: { language?: string; content?: string }): Thenable<TextDocument>;
Expand All @@ -107,7 +96,7 @@ export class MockDocumentManager implements IDocumentManager {
rangeOffset: startOffset,
rangeLength: endOffset - startOffset,
text: c.newText
}
};
});
const ev: TextDocumentChangeEvent = {
document: doc,
Expand Down
99 changes: 99 additions & 0 deletions src/test/datascience/mockTextEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import {
DecorationOptions,
EndOfLine,
Position,
Range,
Selection,
SnippetString,
TextDocument,
TextEditor,
TextEditorDecorationType,
TextEditorEdit,
TextEditorOptions,
TextEditorRevealType,
ViewColumn
} from 'vscode';

import { noop } from '../../client/common/utils/misc';
import { MockDocument } from './mockDocument';
import { MockDocumentManager } from './mockDocumentManager';

class MockEditorEdit implements TextEditorEdit {

constructor(private _documentManager: MockDocumentManager, private _document: MockDocument) {
}

public replace(location: Selection | Range | Position, value: string): void {
this._documentManager.changeDocument(this._document.fileName, [{
range: location as Range,
newText: value
}]);
}

public insert(location: Position, value: string): void {
this._documentManager.changeDocument(this._document.fileName, [{
range: new Range(location, location),
newText: value
}]);
}
public delete(_location: Selection | Range): void {
throw new Error('Method not implemented.');
}
public setEndOfLine(_endOfLine: EndOfLine): void {
throw new Error('Method not implemented.');
}
}

export class MockEditor implements TextEditor {
public selection: Selection;
public selections: Selection[] = [];
private _revealCallback: () => void;

constructor(private _documentManager: MockDocumentManager, private _document: MockDocument) {
this.selection = new Selection(0, 0, 0, 0);
this._revealCallback = noop;
}

public get document(): TextDocument {
return this._document;
}
public get visibleRanges(): Range[] {
return [];
}
public get options(): TextEditorOptions {
return {
};
}
public get viewColumn(): ViewColumn | undefined {
return undefined;
}
public edit(callback: (editBuilder: TextEditorEdit) => void, _options?: { undoStopBefore: boolean; undoStopAfter: boolean } | undefined): Thenable<boolean> {
return new Promise(r => {
const editor = new MockEditorEdit(this._documentManager, this._document);
callback(editor);
r(true);
});
}
public insertSnippet(_snippet: SnippetString, _location?: Range | Position | Range[] | Position[] | undefined, _options?: { undoStopBefore: boolean; undoStopAfter: boolean } | undefined): Thenable<boolean> {
throw new Error('Method not implemented.');
}
public setDecorations(_decorationType: TextEditorDecorationType, _rangesOrOptions: Range[] | DecorationOptions[]): void {
throw new Error('Method not implemented.');
}
public revealRange(_range: Range, _revealType?: TextEditorRevealType | undefined): void {
this._revealCallback();
}
public show(_column?: ViewColumn | undefined): void {
throw new Error('Method not implemented.');
}
public hide(): void {
throw new Error('Method not implemented.');
}

public setRevealCallback(callback: () => void) {
this._revealCallback = callback;
}
}