Skip to content

Commit

Permalink
feat(playgrounds): Show code lens at end of selection (#324)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anemy authored Jul 26, 2021
1 parent deeea70 commit 7bc8c96
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 54 deletions.
51 changes: 51 additions & 0 deletions src/editors/partialExecutionCodeLensProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,57 @@
import * as vscode from 'vscode';
import EXTENSION_COMMANDS from '../commands';

// Returns a boolean if the selection is one that is valid to show a
// code lens for (isn't a partial line etc.).
export function isSelectionValidForCodeLens(
selections: vscode.Selection[],
textDocument: vscode.TextDocument
): boolean {
if (selections.length > 1) {
// Show codelens when it's multi cursor.
return true;
}

if (!selections[0].isSingleLine) {
// Show codelens when it's a multi-line selection.
return true;
}

const lineContent = (textDocument.lineAt(selections[0].end.line).text || '').trim();
const selectionContent = (textDocument.getText(selections[0]) || '').trim();

// Show codelens when it contains the whole line.
return lineContent === selectionContent;
}

export function getCodeLensLineOffsetForSelection(
selections: vscode.Selection[],
editor: vscode.TextEditor
): number {
const lastSelection = selections[selections.length - 1];
const lastSelectedLineNumber = lastSelection.end.line;
const lastSelectedLineContent = editor.document.lineAt(lastSelectedLineNumber).text || '';

// Show a code lens after the selected line, unless the
// contents of the selection in the last line is empty.
const lastSelectedLineContentRange = new vscode.Range(
new vscode.Position(
lastSelection.end.line,
0
),
new vscode.Position(
lastSelection.end.line,
lastSelectedLineContent.length
)
);
const interectedSelection = lastSelection.intersection(lastSelectedLineContentRange);
if (!interectedSelection || interectedSelection.isEmpty) {
return 0;
}

return lastSelectedLineContent.trim().length > 0 ? 1 : 0;
}

export default class PartialExecutionCodeLensProvider
implements vscode.CodeLensProvider {
_selection?: vscode.Range;
Expand Down
82 changes: 52 additions & 30 deletions src/editors/playgroundController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { createLogger } from '../logging';
import { ExplorerController, ConnectionTreeItem, DatabaseTreeItem } from '../explorer';
import { LanguageServerController } from '../language';
import { OutputChannel, ProgressLocation, TextEditor } from 'vscode';
import PartialExecutionCodeLensProvider from './partialExecutionCodeLensProvider';
import PartialExecutionCodeLensProvider, {
isSelectionValidForCodeLens,
getCodeLensLineOffsetForSelection
} from './partialExecutionCodeLensProvider';
import playgroundCreateIndexTemplate from '../templates/playgroundCreateIndexTemplate';
import playgroundCreateCollectionTemplate from '../templates/playgroundCreateCollectionTemplate';
import playgroundCreateCollectionWithTSTemplate from '../templates/playgroundCreateCollectionWithTSTemplate';
Expand Down Expand Up @@ -125,47 +128,66 @@ export default class PlaygroundController {
onDidChangeActiveTextEditor(vscode.window.activeTextEditor);

vscode.window.onDidChangeTextEditorSelection(
(editor: vscode.TextEditorSelectionChangeEvent) => {
(changeEvent: vscode.TextEditorSelectionChangeEvent) => {
if (
editor &&
editor.textEditor &&
editor.textEditor.document &&
editor.textEditor.document.languageId === 'mongodb'
changeEvent?.textEditor?.document?.languageId === 'mongodb'
) {
this._selectedText = (editor.selections as Array<vscode.Selection>)
.sort((a, b) => (a.start.line > b.start.line ? 1 : -1)) // Sort lines selected as alt+click.
.map((item, index) => {
if (index === editor.selections.length - 1) {
this._showCodeLensForSelection(item);
}

return this._getSelectedText(item);
})
// Sort lines selected as the may be mis-ordered from alt+click.
const sortedSelections = (changeEvent.selections as Array<vscode.Selection>)
.sort((a, b) => (a.start.line > b.start.line ? 1 : -1));

this._selectedText = sortedSelections
.map((selection) => this._getSelectedText(selection))
.join('\n');

this._showCodeLensForSelection(
sortedSelections,
changeEvent.textEditor
);
}
}
);
}

_showCodeLensForSelection(item: vscode.Range): void {
const selectedText = this._getSelectedText(item).trim();
const lastSelectedLine =
this._activeTextEditor?.document.lineAt(item.end.line).text.trim() || '';
const selections = this._activeTextEditor?.selections.sort((a, b) =>
a.start.line > b.start.line ? 1 : -1
);
const firstLine = selections ? selections[0].start.line : 0;
_showCodeLensForSelection(
selections: vscode.Selection[],
editor: vscode.TextEditor
): void {
if (!this._selectedText || this._selectedText.trim().length === 0) {
this._partialExecutionCodeLensProvider.refresh();
return;
}

if (!isSelectionValidForCodeLens(selections, editor.document)) {
this._partialExecutionCodeLensProvider.refresh();
return;
}

const lastSelection = selections[selections.length - 1];
const lastSelectedLineNumber = lastSelection.end.line;
const lastSelectedLineContent = editor.document.lineAt(lastSelectedLineNumber).text || '';
// Add an empty line to the end of the file when the selection includes
// the last line and it is not empty.
// We do this so that we can show the code lens after the line's contents.
if (
selectedText.length > 0 &&
selectedText.length >= lastSelectedLine.length
lastSelection.end.line === editor.document.lineCount - 1 &&
lastSelectedLineContent.trim().length > 0
) {
this._partialExecutionCodeLensProvider.refresh(
new vscode.Range(firstLine, 0, firstLine, 0)
);
} else {
this._partialExecutionCodeLensProvider.refresh();
editor.edit(edit => {
edit.insert(
new vscode.Position(lastSelection.end.line + 1, 0),
'\n'
);
});
}

const codeLensLineOffset = getCodeLensLineOffsetForSelection(selections, editor);
this._partialExecutionCodeLensProvider.refresh(
new vscode.Range(
lastSelectedLineNumber + codeLensLineOffset, 0,
lastSelectedLineNumber + codeLensLineOffset, 0
)
);
}

_disconnectFromServiceProvider(): void {
Expand Down
149 changes: 125 additions & 24 deletions src/test/suite/editors/playgroundController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,30 @@ const CONNECTION = {
driverOptions: {}
};

const mockPlayground = 'use(\'dbName\');\n a\n\n\n\ndb.find();';

const activeTestEditorWithSelectionMock = {
document: {
languageId: 'mongodb',
uri: {
path: 'test'
} as vscode.Uri,
getText: (range: vscode.Range) => mockPlayground.split('\n')[range.start.line].substr(range.start.character, range.end.character),
lineAt: (lineNumber: number) => ({
text: mockPlayground.split('\n')[lineNumber]
}),
lineCount: mockPlayground.split('\n').length
},
selections: [
new vscode.Selection(
new vscode.Position(0, 5),
new vscode.Position(0, 11)
)
],
edit: () => null
} as unknown as vscode.TextEditor;


suite('Playground Controller Test Suite', function () {
this.timeout(5000);

Expand Down Expand Up @@ -443,43 +467,120 @@ suite('Playground Controller Test Suite', function () {
});
});

test('do not show code lens if a part of a line is selected', () => {
const activeTestEditorWithSelectionMock: unknown = {
document: {
languageId: 'mongodb',
uri: {
path: 'test'
},
getText: () => 'dbName',
lineAt: sinon.fake.returns({ text: "use('dbName');" })
},
selections: [
new vscode.Selection(
new vscode.Position(0, 5),
new vscode.Position(0, 11)
)
]
};

testPlaygroundController._activeTextEditor = activeTestEditorWithSelectionMock as vscode.TextEditor;
test('do not show code lens if a part of a line with content is selected', () => {
testPlaygroundController._selectedText = 'db';
testPlaygroundController._activeTextEditor = activeTestEditorWithSelectionMock;

testPlaygroundController._showCodeLensForSelection(
new vscode.Range(0, 5, 0, 11)
[new vscode.Selection(
new vscode.Position(0, 5),
new vscode.Position(0, 11)
)],
activeTestEditorWithSelectionMock
);

const codeLens = testPlaygroundController._partialExecutionCodeLensProvider?.provideCodeLenses();

expect(codeLens?.length).to.be.equal(0);
expect(codeLens.length).to.be.equal(0);
});

test('show code lens if whole line is selected', () => {
test('do not show code lens when it has no content (multi-line)', () => {
testPlaygroundController._selectedText = ' \n\n ';
testPlaygroundController._activeTextEditor = activeTestEditorWithSelectionMock;

testPlaygroundController._showCodeLensForSelection(
new vscode.Range(0, 0, 0, 14)
[new vscode.Selection(
new vscode.Position(2, 0),
new vscode.Position(4, 0)
)],
activeTestEditorWithSelectionMock
);

const codeLens = testPlaygroundController._partialExecutionCodeLensProvider?.provideCodeLenses();

expect(codeLens?.length).to.be.equal(1);
expect(codeLens.length).to.be.equal(0);
});

test('show code lens if whole line is selected', () => {
testPlaygroundController._selectedText = 'use(\'dbName\');';
testPlaygroundController._showCodeLensForSelection(
[new vscode.Selection(
new vscode.Position(0, 0),
new vscode.Position(0, 15)
)],
activeTestEditorWithSelectionMock
);

const codeLens = testPlaygroundController._partialExecutionCodeLensProvider.provideCodeLenses();

expect(codeLens.length).to.be.equal(1);
expect(codeLens[0].range.end.line).to.be.equal(1);
});

test('has the correct line number for code lens when the selection includes the last line', () => {
testPlaygroundController._selectedText = 'use(\'dbName\');\n\na';
const fakeEdit = sinon.fake.returns(() => ({ insert: sinon.fake() }));
sandbox.replace(
activeTestEditorWithSelectionMock,
'edit',
fakeEdit
);
testPlaygroundController._showCodeLensForSelection(
[new vscode.Selection(
new vscode.Position(0, 0),
new vscode.Position(5, 5)
)],
activeTestEditorWithSelectionMock
);

const codeLens = testPlaygroundController._partialExecutionCodeLensProvider.provideCodeLenses();

expect(codeLens.length).to.be.equal(1);
expect(codeLens[0].range.start.line).to.be.equal(6);
expect(codeLens[0].range.end.line).to.be.equal(6);
expect(fakeEdit).to.be.called;
});

test('it calls to edit the document to add an empty line if the selection includes the last line with content', (done) => {
testPlaygroundController._selectedText = 'use(\'dbName\');\n\na';
sandbox.replace(
activeTestEditorWithSelectionMock,
'edit',
(cb) => {
cb({
insert: (position: vscode.Position, toInsert: string) => {
expect(position.line).to.equal(6);
expect(toInsert).to.equal('\n');
done();
}
} as unknown as vscode.TextEditorEdit);
return new Promise((resolve) => resolve(true));
}
);
testPlaygroundController._showCodeLensForSelection(
[new vscode.Selection(
new vscode.Position(0, 0),
new vscode.Position(5, 5)
)],
activeTestEditorWithSelectionMock
);
});

test('shows the codelens on the line above the last selected line when the last selected line is empty', () => {
testPlaygroundController._selectedText = 'use(\'dbName\');\n\n';
testPlaygroundController._showCodeLensForSelection(
[new vscode.Selection(
new vscode.Position(0, 0),
new vscode.Position(1, 3)
)],
activeTestEditorWithSelectionMock
);

const codeLens = testPlaygroundController._partialExecutionCodeLensProvider.provideCodeLenses();

expect(codeLens.length).to.be.equal(1);
expect(codeLens[0].range.start.line).to.be.equal(2);
expect(codeLens[0].range.end.line).to.be.equal(2);
});

test('playground controller loads the active editor on start', () => {
Expand Down

0 comments on commit 7bc8c96

Please sign in to comment.