Skip to content

Commit 7bc8c96

Browse files
authored
feat(playgrounds): Show code lens at end of selection (#324)
1 parent deeea70 commit 7bc8c96

File tree

3 files changed

+228
-54
lines changed

3 files changed

+228
-54
lines changed

src/editors/partialExecutionCodeLensProvider.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,57 @@
11
import * as vscode from 'vscode';
22
import EXTENSION_COMMANDS from '../commands';
33

4+
// Returns a boolean if the selection is one that is valid to show a
5+
// code lens for (isn't a partial line etc.).
6+
export function isSelectionValidForCodeLens(
7+
selections: vscode.Selection[],
8+
textDocument: vscode.TextDocument
9+
): boolean {
10+
if (selections.length > 1) {
11+
// Show codelens when it's multi cursor.
12+
return true;
13+
}
14+
15+
if (!selections[0].isSingleLine) {
16+
// Show codelens when it's a multi-line selection.
17+
return true;
18+
}
19+
20+
const lineContent = (textDocument.lineAt(selections[0].end.line).text || '').trim();
21+
const selectionContent = (textDocument.getText(selections[0]) || '').trim();
22+
23+
// Show codelens when it contains the whole line.
24+
return lineContent === selectionContent;
25+
}
26+
27+
export function getCodeLensLineOffsetForSelection(
28+
selections: vscode.Selection[],
29+
editor: vscode.TextEditor
30+
): number {
31+
const lastSelection = selections[selections.length - 1];
32+
const lastSelectedLineNumber = lastSelection.end.line;
33+
const lastSelectedLineContent = editor.document.lineAt(lastSelectedLineNumber).text || '';
34+
35+
// Show a code lens after the selected line, unless the
36+
// contents of the selection in the last line is empty.
37+
const lastSelectedLineContentRange = new vscode.Range(
38+
new vscode.Position(
39+
lastSelection.end.line,
40+
0
41+
),
42+
new vscode.Position(
43+
lastSelection.end.line,
44+
lastSelectedLineContent.length
45+
)
46+
);
47+
const interectedSelection = lastSelection.intersection(lastSelectedLineContentRange);
48+
if (!interectedSelection || interectedSelection.isEmpty) {
49+
return 0;
50+
}
51+
52+
return lastSelectedLineContent.trim().length > 0 ? 1 : 0;
53+
}
54+
455
export default class PartialExecutionCodeLensProvider
556
implements vscode.CodeLensProvider {
657
_selection?: vscode.Range;

src/editors/playgroundController.ts

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { createLogger } from '../logging';
99
import { ExplorerController, ConnectionTreeItem, DatabaseTreeItem } from '../explorer';
1010
import { LanguageServerController } from '../language';
1111
import { OutputChannel, ProgressLocation, TextEditor } from 'vscode';
12-
import PartialExecutionCodeLensProvider from './partialExecutionCodeLensProvider';
12+
import PartialExecutionCodeLensProvider, {
13+
isSelectionValidForCodeLens,
14+
getCodeLensLineOffsetForSelection
15+
} from './partialExecutionCodeLensProvider';
1316
import playgroundCreateIndexTemplate from '../templates/playgroundCreateIndexTemplate';
1417
import playgroundCreateCollectionTemplate from '../templates/playgroundCreateCollectionTemplate';
1518
import playgroundCreateCollectionWithTSTemplate from '../templates/playgroundCreateCollectionWithTSTemplate';
@@ -125,47 +128,66 @@ export default class PlaygroundController {
125128
onDidChangeActiveTextEditor(vscode.window.activeTextEditor);
126129

127130
vscode.window.onDidChangeTextEditorSelection(
128-
(editor: vscode.TextEditorSelectionChangeEvent) => {
131+
(changeEvent: vscode.TextEditorSelectionChangeEvent) => {
129132
if (
130-
editor &&
131-
editor.textEditor &&
132-
editor.textEditor.document &&
133-
editor.textEditor.document.languageId === 'mongodb'
133+
changeEvent?.textEditor?.document?.languageId === 'mongodb'
134134
) {
135-
this._selectedText = (editor.selections as Array<vscode.Selection>)
136-
.sort((a, b) => (a.start.line > b.start.line ? 1 : -1)) // Sort lines selected as alt+click.
137-
.map((item, index) => {
138-
if (index === editor.selections.length - 1) {
139-
this._showCodeLensForSelection(item);
140-
}
141-
142-
return this._getSelectedText(item);
143-
})
135+
// Sort lines selected as the may be mis-ordered from alt+click.
136+
const sortedSelections = (changeEvent.selections as Array<vscode.Selection>)
137+
.sort((a, b) => (a.start.line > b.start.line ? 1 : -1));
138+
139+
this._selectedText = sortedSelections
140+
.map((selection) => this._getSelectedText(selection))
144141
.join('\n');
142+
143+
this._showCodeLensForSelection(
144+
sortedSelections,
145+
changeEvent.textEditor
146+
);
145147
}
146148
}
147149
);
148150
}
149151

150-
_showCodeLensForSelection(item: vscode.Range): void {
151-
const selectedText = this._getSelectedText(item).trim();
152-
const lastSelectedLine =
153-
this._activeTextEditor?.document.lineAt(item.end.line).text.trim() || '';
154-
const selections = this._activeTextEditor?.selections.sort((a, b) =>
155-
a.start.line > b.start.line ? 1 : -1
156-
);
157-
const firstLine = selections ? selections[0].start.line : 0;
152+
_showCodeLensForSelection(
153+
selections: vscode.Selection[],
154+
editor: vscode.TextEditor
155+
): void {
156+
if (!this._selectedText || this._selectedText.trim().length === 0) {
157+
this._partialExecutionCodeLensProvider.refresh();
158+
return;
159+
}
160+
161+
if (!isSelectionValidForCodeLens(selections, editor.document)) {
162+
this._partialExecutionCodeLensProvider.refresh();
163+
return;
164+
}
158165

166+
const lastSelection = selections[selections.length - 1];
167+
const lastSelectedLineNumber = lastSelection.end.line;
168+
const lastSelectedLineContent = editor.document.lineAt(lastSelectedLineNumber).text || '';
169+
// Add an empty line to the end of the file when the selection includes
170+
// the last line and it is not empty.
171+
// We do this so that we can show the code lens after the line's contents.
159172
if (
160-
selectedText.length > 0 &&
161-
selectedText.length >= lastSelectedLine.length
173+
lastSelection.end.line === editor.document.lineCount - 1 &&
174+
lastSelectedLineContent.trim().length > 0
162175
) {
163-
this._partialExecutionCodeLensProvider.refresh(
164-
new vscode.Range(firstLine, 0, firstLine, 0)
165-
);
166-
} else {
167-
this._partialExecutionCodeLensProvider.refresh();
176+
editor.edit(edit => {
177+
edit.insert(
178+
new vscode.Position(lastSelection.end.line + 1, 0),
179+
'\n'
180+
);
181+
});
168182
}
183+
184+
const codeLensLineOffset = getCodeLensLineOffsetForSelection(selections, editor);
185+
this._partialExecutionCodeLensProvider.refresh(
186+
new vscode.Range(
187+
lastSelectedLineNumber + codeLensLineOffset, 0,
188+
lastSelectedLineNumber + codeLensLineOffset, 0
189+
)
190+
);
169191
}
170192

171193
_disconnectFromServiceProvider(): void {

src/test/suite/editors/playgroundController.test.ts

Lines changed: 125 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,30 @@ const CONNECTION = {
2626
driverOptions: {}
2727
};
2828

29+
const mockPlayground = 'use(\'dbName\');\n a\n\n\n\ndb.find();';
30+
31+
const activeTestEditorWithSelectionMock = {
32+
document: {
33+
languageId: 'mongodb',
34+
uri: {
35+
path: 'test'
36+
} as vscode.Uri,
37+
getText: (range: vscode.Range) => mockPlayground.split('\n')[range.start.line].substr(range.start.character, range.end.character),
38+
lineAt: (lineNumber: number) => ({
39+
text: mockPlayground.split('\n')[lineNumber]
40+
}),
41+
lineCount: mockPlayground.split('\n').length
42+
},
43+
selections: [
44+
new vscode.Selection(
45+
new vscode.Position(0, 5),
46+
new vscode.Position(0, 11)
47+
)
48+
],
49+
edit: () => null
50+
} as unknown as vscode.TextEditor;
51+
52+
2953
suite('Playground Controller Test Suite', function () {
3054
this.timeout(5000);
3155

@@ -443,43 +467,120 @@ suite('Playground Controller Test Suite', function () {
443467
});
444468
});
445469

446-
test('do not show code lens if a part of a line is selected', () => {
447-
const activeTestEditorWithSelectionMock: unknown = {
448-
document: {
449-
languageId: 'mongodb',
450-
uri: {
451-
path: 'test'
452-
},
453-
getText: () => 'dbName',
454-
lineAt: sinon.fake.returns({ text: "use('dbName');" })
455-
},
456-
selections: [
457-
new vscode.Selection(
458-
new vscode.Position(0, 5),
459-
new vscode.Position(0, 11)
460-
)
461-
]
462-
};
463-
464-
testPlaygroundController._activeTextEditor = activeTestEditorWithSelectionMock as vscode.TextEditor;
470+
test('do not show code lens if a part of a line with content is selected', () => {
471+
testPlaygroundController._selectedText = 'db';
472+
testPlaygroundController._activeTextEditor = activeTestEditorWithSelectionMock;
465473

466474
testPlaygroundController._showCodeLensForSelection(
467-
new vscode.Range(0, 5, 0, 11)
475+
[new vscode.Selection(
476+
new vscode.Position(0, 5),
477+
new vscode.Position(0, 11)
478+
)],
479+
activeTestEditorWithSelectionMock
468480
);
469481

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

472-
expect(codeLens?.length).to.be.equal(0);
484+
expect(codeLens.length).to.be.equal(0);
473485
});
474486

475-
test('show code lens if whole line is selected', () => {
487+
test('do not show code lens when it has no content (multi-line)', () => {
488+
testPlaygroundController._selectedText = ' \n\n ';
489+
testPlaygroundController._activeTextEditor = activeTestEditorWithSelectionMock;
490+
476491
testPlaygroundController._showCodeLensForSelection(
477-
new vscode.Range(0, 0, 0, 14)
492+
[new vscode.Selection(
493+
new vscode.Position(2, 0),
494+
new vscode.Position(4, 0)
495+
)],
496+
activeTestEditorWithSelectionMock
478497
);
479498

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

482-
expect(codeLens?.length).to.be.equal(1);
501+
expect(codeLens.length).to.be.equal(0);
502+
});
503+
504+
test('show code lens if whole line is selected', () => {
505+
testPlaygroundController._selectedText = 'use(\'dbName\');';
506+
testPlaygroundController._showCodeLensForSelection(
507+
[new vscode.Selection(
508+
new vscode.Position(0, 0),
509+
new vscode.Position(0, 15)
510+
)],
511+
activeTestEditorWithSelectionMock
512+
);
513+
514+
const codeLens = testPlaygroundController._partialExecutionCodeLensProvider.provideCodeLenses();
515+
516+
expect(codeLens.length).to.be.equal(1);
517+
expect(codeLens[0].range.end.line).to.be.equal(1);
518+
});
519+
520+
test('has the correct line number for code lens when the selection includes the last line', () => {
521+
testPlaygroundController._selectedText = 'use(\'dbName\');\n\na';
522+
const fakeEdit = sinon.fake.returns(() => ({ insert: sinon.fake() }));
523+
sandbox.replace(
524+
activeTestEditorWithSelectionMock,
525+
'edit',
526+
fakeEdit
527+
);
528+
testPlaygroundController._showCodeLensForSelection(
529+
[new vscode.Selection(
530+
new vscode.Position(0, 0),
531+
new vscode.Position(5, 5)
532+
)],
533+
activeTestEditorWithSelectionMock
534+
);
535+
536+
const codeLens = testPlaygroundController._partialExecutionCodeLensProvider.provideCodeLenses();
537+
538+
expect(codeLens.length).to.be.equal(1);
539+
expect(codeLens[0].range.start.line).to.be.equal(6);
540+
expect(codeLens[0].range.end.line).to.be.equal(6);
541+
expect(fakeEdit).to.be.called;
542+
});
543+
544+
test('it calls to edit the document to add an empty line if the selection includes the last line with content', (done) => {
545+
testPlaygroundController._selectedText = 'use(\'dbName\');\n\na';
546+
sandbox.replace(
547+
activeTestEditorWithSelectionMock,
548+
'edit',
549+
(cb) => {
550+
cb({
551+
insert: (position: vscode.Position, toInsert: string) => {
552+
expect(position.line).to.equal(6);
553+
expect(toInsert).to.equal('\n');
554+
done();
555+
}
556+
} as unknown as vscode.TextEditorEdit);
557+
return new Promise((resolve) => resolve(true));
558+
}
559+
);
560+
testPlaygroundController._showCodeLensForSelection(
561+
[new vscode.Selection(
562+
new vscode.Position(0, 0),
563+
new vscode.Position(5, 5)
564+
)],
565+
activeTestEditorWithSelectionMock
566+
);
567+
});
568+
569+
test('shows the codelens on the line above the last selected line when the last selected line is empty', () => {
570+
testPlaygroundController._selectedText = 'use(\'dbName\');\n\n';
571+
testPlaygroundController._showCodeLensForSelection(
572+
[new vscode.Selection(
573+
new vscode.Position(0, 0),
574+
new vscode.Position(1, 3)
575+
)],
576+
activeTestEditorWithSelectionMock
577+
);
578+
579+
const codeLens = testPlaygroundController._partialExecutionCodeLensProvider.provideCodeLenses();
580+
581+
expect(codeLens.length).to.be.equal(1);
582+
expect(codeLens[0].range.start.line).to.be.equal(2);
583+
expect(codeLens[0].range.end.line).to.be.equal(2);
483584
});
484585

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

0 commit comments

Comments
 (0)