-
Notifications
You must be signed in to change notification settings - Fork 297
/
codewatcher.ts
1138 lines (1021 loc) · 45.6 KB
/
codewatcher.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import type * as nbformat from '@jupyterlab/nbformat';
import { inject, injectable } from 'inversify';
import {
CodeLens,
commands,
Event,
EventEmitter,
Position,
Range,
Selection,
TextDocument,
TextEditor,
TextEditorRevealType,
Uri
} from 'vscode';
import { IDocumentManager } from '../../common/application/types';
import { IFileSystem } from '../../common/platform/types';
import { IConfigurationService, IDisposable, IJupyterSettings, Resource } from '../../common/types';
import * as localize from '../../common/utils/localize';
import { isUri } from '../../common/utils/misc';
import { StopWatch } from '../../common/utils/stopWatch';
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
import { ICodeExecutionHelper } from '../../terminals/types';
import { CellMatcher } from '../cellMatcher';
import { Commands, Identifiers, Telemetry } from '../constants';
import {
ICellRange,
ICodeLensFactory,
ICodeWatcher,
IDataScienceErrorHandler,
IInteractiveWindowProvider
} from '../types';
function getIndex(index: number, length: number): number {
// return index within the length range with negative indexing
if (length <= 0) {
throw new RangeError(`Length must be > 0 not ${length}`);
}
// negative index count back from length
if (index < 0) {
index += length;
}
// bounded index
if (index < 0) {
return 0;
} else if (index >= length) {
return length - 1;
} else {
return index;
}
}
@injectable()
export class CodeWatcher implements ICodeWatcher {
private static sentExecuteCellTelemetry: boolean = false;
private document?: TextDocument;
private version: number = -1;
private codeLenses: CodeLens[] = [];
private cells: ICellRange[] = [];
private cachedSettings: IJupyterSettings | undefined;
private codeLensUpdatedEvent: EventEmitter<void> = new EventEmitter<void>();
private updateRequiredDisposable: IDisposable | undefined;
private closeDocumentDisposable: IDisposable | undefined;
constructor(
@inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider,
@inject(IFileSystem) private fs: IFileSystem,
@inject(IConfigurationService) private configService: IConfigurationService,
@inject(IDocumentManager) private documentManager: IDocumentManager,
@inject(ICodeExecutionHelper) private executionHelper: ICodeExecutionHelper,
@inject(IDataScienceErrorHandler) protected dataScienceErrorHandler: IDataScienceErrorHandler,
@inject(ICodeLensFactory) private codeLensFactory: ICodeLensFactory
) {}
public setDocument(document: TextDocument) {
this.document = document;
// Cache the version, we don't want to pull an old version if the document is updated
this.version = document.version;
// Get document cells here. Make a copy of our settings.
this.cachedSettings = JSON.parse(JSON.stringify(this.configService.getSettings(document.uri)));
// Use the factory to generate our new code lenses.
this.codeLenses = this.codeLensFactory.createCodeLenses(document);
this.cells = this.codeLensFactory.getCellRanges(document);
// Listen for changes
this.updateRequiredDisposable = this.codeLensFactory.updateRequired(this.onCodeLensFactoryUpdated.bind(this));
// Make sure to stop listening for changes when this document closes.
this.closeDocumentDisposable = this.documentManager.onDidCloseTextDocument(this.onDocumentClosed.bind(this));
}
public get codeLensUpdated(): Event<void> {
return this.codeLensUpdatedEvent.event;
}
public get uri() {
return this.document?.uri;
}
public getVersion() {
return this.version;
}
public getCachedSettings(): IJupyterSettings | undefined {
return this.cachedSettings;
}
public getCodeLenses() {
return this.codeLenses;
}
@captureTelemetry(Telemetry.DebugCurrentCell)
public async debugCurrentCell() {
if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) {
return;
}
// Run the cell that matches the current cursor position.
return this.runMatchingCell(this.documentManager.activeTextEditor.selection, false, true);
}
public dispose() {
this.codeLensUpdatedEvent.dispose();
this.closeDocumentDisposable?.dispose(); // NOSONAR
this.updateRequiredDisposable?.dispose(); // NOSONAR
}
@captureTelemetry(Telemetry.RunAllCells)
public async runAllCells() {
const runCellCommands = this.codeLenses.filter(
(c) =>
c.command &&
c.command.command === Commands.RunCell &&
c.command.arguments &&
c.command.arguments.length >= 5
);
let leftCount = runCellCommands.length;
// Run all of our code lenses, they should always be ordered in the file so we can just
// run them one by one
for (const lens of runCellCommands) {
// Make sure that we have the correct command (RunCell) lenses
let range: Range = new Range(
lens.command!.arguments![1],
lens.command!.arguments![2],
lens.command!.arguments![3],
lens.command!.arguments![4]
);
if (this.document) {
// Special case, if this is the first, expand our range to always include the top.
if (leftCount === runCellCommands.length) {
range = new Range(new Position(0, 0), range.end);
}
const code = this.document.getText(range);
leftCount -= 1;
// Note: We do a get or create active before all addCode commands to make sure that we either have a history up already
// or if we do not we need to start it up as these commands are all expected to start a new history if needed
const success = await this.addCode(code, this.document.uri, range.start.line);
if (!success) {
await this.addErrorMessage(this.document.uri, leftCount);
break;
}
}
}
// If there are no codelenses, just run all of the code as a single cell
if (runCellCommands.length === 0) {
return this.runFileInteractiveInternal(false);
}
}
@captureTelemetry(Telemetry.RunFileInteractive)
public async runFileInteractive() {
return this.runFileInteractiveInternal(false);
}
@captureTelemetry(Telemetry.DebugFileInteractive)
public async debugFileInteractive() {
return this.runFileInteractiveInternal(true);
}
// Run all cells up to the cell containing this start line and character
@captureTelemetry(Telemetry.RunAllCellsAbove)
public async runAllCellsAbove(stopLine: number, stopCharacter: number) {
const runCellCommands = this.codeLenses.filter((c) => c.command && c.command.command === Commands.RunCell);
let leftCount = runCellCommands.findIndex(
(c) => c.range.start.line >= stopLine && c.range.start.character >= stopCharacter
);
if (leftCount < 0) {
leftCount = runCellCommands.length;
}
const startCount = leftCount;
// Run our code lenses up to this point, lenses are created in order on document load
// so we can rely on them being in linear order for this
for (const lens of runCellCommands) {
// Make sure we are dealing with run cell based code lenses in case more types are added later
if (leftCount > 0 && this.document) {
let range: Range = new Range(lens.range.start, lens.range.end);
// If this is the first, make sure it extends to the top
if (leftCount === startCount) {
range = new Range(new Position(0, 0), range.end);
}
// We have a cell and we are not past or at the stop point
leftCount -= 1;
const code = this.document.getText(range);
const success = await this.addCode(code, this.document.uri, lens.range.start.line);
if (!success) {
await this.addErrorMessage(this.document.uri, leftCount);
break;
}
} else {
// If we get a cell past or at the stop point stop
break;
}
}
}
@captureTelemetry(Telemetry.RunCellAndAllBelow)
public async runCellAndAllBelow(startLine: number, startCharacter: number) {
const runCellCommands = this.codeLenses.filter((c) => c.command && c.command.command === Commands.RunCell);
const index = runCellCommands.findIndex(
(c) => c.range.start.line >= startLine && c.range.start.character >= startCharacter
);
let leftCount = index > 0 ? runCellCommands.length - index : runCellCommands.length;
// Run our code lenses from this point to the end, lenses are created in order on document load
// so we can rely on them being in linear order for this
for (let pos = index; pos >= 0 && pos < runCellCommands.length; pos += 1) {
if (leftCount > 0 && this.document) {
const lens = runCellCommands[pos];
// We have a cell and we are not past or at the stop point
leftCount -= 1;
const code = this.document.getText(lens.range);
const success = await this.addCode(code, this.document.uri, lens.range.start.line);
if (!success) {
await this.addErrorMessage(this.document.uri, leftCount);
break;
}
}
}
}
@captureTelemetry(Telemetry.RunSelectionOrLine)
public async runSelectionOrLine(activeEditor: TextEditor | undefined, text?: string | Uri) {
if (this.document && activeEditor && this.fs.arePathsSame(activeEditor.document.uri, this.document.uri)) {
let codeToExecute: string | undefined;
if (text === undefined || isUri(text)) {
// Get just the text of the selection or the current line if none
codeToExecute = await this.executionHelper.getSelectedTextToExecute(activeEditor);
} else {
codeToExecute = text;
}
if (!codeToExecute) {
return;
}
const normalizedCode = await this.executionHelper.normalizeLines(codeToExecute!);
if (!normalizedCode || normalizedCode.trim().length === 0) {
return;
}
await this.addCode(normalizedCode, this.document.uri, activeEditor.selection.start.line, activeEditor);
}
}
@captureTelemetry(Telemetry.RunToLine)
public async runToLine(targetLine: number) {
if (this.document && targetLine > 0) {
const previousLine = this.document.lineAt(targetLine - 1);
const code = this.document.getText(
new Range(0, 0, previousLine.range.end.line, previousLine.range.end.character)
);
if (code && code.trim().length) {
await this.addCode(code, this.document.uri, 0);
}
}
}
@captureTelemetry(Telemetry.RunFromLine)
public async runFromLine(targetLine: number) {
if (this.document && targetLine < this.document.lineCount) {
const lastLine = this.document.lineAt(this.document.lineCount - 1);
const code = this.document.getText(
new Range(targetLine, 0, lastLine.range.end.line, lastLine.range.end.character)
);
if (code && code.trim().length) {
await this.addCode(code, this.document.uri, targetLine);
}
}
}
@captureTelemetry(Telemetry.RunCell)
public async runCell(range: Range): Promise<void> {
if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) {
return;
}
// Run the cell clicked. Advance if the cursor is inside this cell and we're allowed to.
const advance =
range.contains(this.documentManager.activeTextEditor.selection.start) &&
this.configService.getSettings(this.documentManager.activeTextEditor.document.uri).enableAutoMoveToNextCell;
return this.runMatchingCell(range, advance);
}
@captureTelemetry(Telemetry.DebugCurrentCell)
public async debugCell(range: Range): Promise<void> {
if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) {
return;
}
// Debug the cell clicked.
return this.runMatchingCell(range, false, true);
}
@captureTelemetry(Telemetry.RunCurrentCell)
public async runCurrentCell(): Promise<void> {
if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) {
return;
}
// Run the cell that matches the current cursor position.
return this.runMatchingCell(this.documentManager.activeTextEditor.selection, false);
}
@captureTelemetry(Telemetry.RunCurrentCellAndAdvance)
public async runCurrentCellAndAdvance() {
if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) {
return;
}
// Run the cell that matches the current cursor position. Always advance
return this.runMatchingCell(this.documentManager.activeTextEditor.selection, true);
}
// telemetry captured on CommandRegistry
public async addEmptyCellToBottom(): Promise<void> {
const editor = this.documentManager.activeTextEditor;
if (editor) {
this.insertCell(editor, editor.document.lineCount + 1);
}
}
@captureTelemetry(Telemetry.RunCurrentCellAndAddBelow)
public async runCurrentCellAndAddBelow(): Promise<void> {
if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) {
return;
}
const editor = this.documentManager.activeTextEditor;
const cellMatcher = new CellMatcher();
let index = 0;
const cellDelineator = this.getDefaultCellMarker(editor.document.uri);
const { newCellOnRunLast } = this.configService.getSettings(this.documentManager.activeTextEditor.document.uri);
if (editor) {
void editor.edit((editBuilder) => {
let lastCell = true;
for (let i = editor.selection.end.line + 1; i < editor.document.lineCount; i += 1) {
if (cellMatcher.isCell(editor.document.lineAt(i).text)) {
lastCell = false;
index = i;
editBuilder.insert(new Position(i, 0), `${cellDelineator}\n\n`);
break;
}
}
if (lastCell) {
index = editor.document.lineCount;
if (newCellOnRunLast) {
editBuilder.insert(new Position(editor.document.lineCount, 0), `\n${cellDelineator}\n`);
}
}
});
}
// Run the cell that matches the current cursor position, and then advance to the new cell
const newPosition = new Position(index + 1, 0);
return this.runMatchingCell(editor.selection, false).then(() =>
this.advanceToRange(new Range(newPosition, newPosition))
);
}
@captureTelemetry(Telemetry.InsertCellBelowPosition)
public insertCellBelowPosition() {
const editor = this.documentManager.activeTextEditor;
if (editor && editor.selection) {
this.insertCell(editor, editor.selection.end.line + 1);
}
}
@captureTelemetry(Telemetry.InsertCellBelow)
public insertCellBelow() {
const editor = this.documentManager.activeTextEditor;
if (editor && editor.selection) {
const cell = this.getCellFromPosition(editor.selection.end);
if (cell) {
this.insertCell(editor, cell.range.end.line + 1);
} else {
this.insertCell(editor, editor.selection.end.line + 1);
}
}
}
@captureTelemetry(Telemetry.InsertCellAbove)
public insertCellAbove() {
const editor = this.documentManager.activeTextEditor;
if (editor && editor.selection) {
const cell = this.getCellFromPosition(editor.selection.start);
if (cell) {
this.insertCell(editor, cell.range.start.line);
} else {
this.insertCell(editor, editor.selection.start.line);
}
}
}
@captureTelemetry(Telemetry.DeleteCells)
public deleteCells() {
const editor = this.documentManager.activeTextEditor;
if (!editor || !editor.selection) {
return;
}
const firstLastCells = this.getStartEndCells(editor.selection);
if (!firstLastCells) {
return;
}
const startCell = firstLastCells[0];
const endCell = firstLastCells[1];
// Start of the document should start at position 0, 0 and end one line ahead.
let startLineNumber = 0;
let startCharacterNumber = 0;
let endLineNumber = endCell.range.end.line + 1;
let endCharacterNumber = 0;
// Anywhere else in the document should start at the end of line before the
// cell and end at the last character of the cell.
if (startCell.range.start.line > 0) {
startLineNumber = startCell.range.start.line - 1;
startCharacterNumber = editor.document.lineAt(startLineNumber).range.end.character;
endLineNumber = endCell.range.end.line;
endCharacterNumber = endCell.range.end.character;
}
const cellExtendedRange = new Range(
new Position(startLineNumber, startCharacterNumber),
new Position(endLineNumber, endCharacterNumber)
);
void editor.edit((editBuilder) => {
editBuilder.replace(cellExtendedRange, '');
this.codeLensUpdatedEvent.fire();
});
}
@captureTelemetry(Telemetry.SelectCell)
public selectCell() {
const editor = this.documentManager.activeTextEditor;
if (editor && editor.selection) {
const startEndCells = this.getStartEndCells(editor.selection);
if (startEndCells) {
const startCell = startEndCells[0];
const endCell = startEndCells[1];
if (editor.selection.anchor.isBeforeOrEqual(editor.selection.active)) {
editor.selection = new Selection(startCell.range.start, endCell.range.end);
} else {
editor.selection = new Selection(endCell.range.end, startCell.range.start);
}
}
}
}
@captureTelemetry(Telemetry.SelectCellContents)
public selectCellContents() {
const editor = this.documentManager.activeTextEditor;
if (!editor || !editor.selection) {
return;
}
const startEndCellIndex = this.getStartEndCellIndex(editor.selection);
if (!startEndCellIndex) {
return;
}
const startCellIndex = startEndCellIndex[0];
const endCellIndex = startEndCellIndex[1];
const isAnchorLessEqualActive = editor.selection.anchor.isBeforeOrEqual(editor.selection.active);
const cells = this.cells;
const selections: Selection[] = [];
for (let i = startCellIndex; i <= endCellIndex; i += 1) {
const cell = cells[i];
let anchorLine = cell.range.start.line + 1;
let achorCharacter = 0;
let activeLine = cell.range.end.line;
let activeCharacter = cell.range.end.character;
// if cell is only one line long, select the end of that line
if (cell.range.start.line === cell.range.end.line) {
anchorLine = cell.range.start.line;
achorCharacter = editor.document.lineAt(anchorLine).range.end.character;
activeLine = anchorLine;
activeCharacter = achorCharacter;
}
if (isAnchorLessEqualActive) {
selections.push(new Selection(anchorLine, achorCharacter, activeLine, activeCharacter));
} else {
selections.push(new Selection(activeLine, activeCharacter, anchorLine, achorCharacter));
}
}
editor.selections = selections;
}
@captureTelemetry(Telemetry.ExtendSelectionByCellAbove)
public extendSelectionByCellAbove() {
// This behaves similarly to excel "Extend Selection by One Cell Above".
// The direction of the selection matters (i.e. where the active cursor)
// position is. First, it ensures that complete cells are selection.
// If so, then if active cursor is in cells below it contracts the
// selection range. If the active cursor is above, it expands the
// selection range.
const editor = this.documentManager.activeTextEditor;
if (!editor || !editor.selection) {
return;
}
const currentSelection = editor.selection;
const startEndCellIndex = this.getStartEndCellIndex(editor.selection);
if (!startEndCellIndex) {
return;
}
const isAnchorLessThanActive = editor.selection.anchor.isBefore(editor.selection.active);
const cells = this.cells;
const startCellIndex = startEndCellIndex[0];
const endCellIndex = startEndCellIndex[1];
const startCell = cells[startCellIndex];
const endCell = cells[endCellIndex];
if (
!startCell.range.start.isEqual(currentSelection.start) ||
!endCell.range.end.isEqual(currentSelection.end)
) {
// full cell range not selected, first select a full cell range.
let selection: Selection;
if (isAnchorLessThanActive) {
if (startCellIndex < endCellIndex) {
// active at end of cell before endCell
selection = new Selection(startCell.range.start, cells[endCellIndex - 1].range.end);
} else {
// active at end of startCell
selection = new Selection(startCell.range.end, startCell.range.start);
}
} else {
// active at start of start cell.
selection = new Selection(endCell.range.end, startCell.range.start);
}
editor.selection = selection;
} else {
let newCell: ICellRange | undefined;
// full cell range is selected now decide if expanding or contracting?
if (isAnchorLessThanActive && startCellIndex < endCellIndex) {
// anchor is above active, contract selection by cell below.
newCell = cells[endCellIndex - 1];
editor.selection = new Selection(startCell.range.start, newCell.range.end);
} else {
// anchor is below active, expand selection by cell above.
if (startCellIndex > 0) {
newCell = cells[startCellIndex - 1];
editor.selection = new Selection(endCell.range.end, newCell.range.start);
}
}
if (newCell) {
editor.revealRange(newCell.range, TextEditorRevealType.Default);
}
}
}
@captureTelemetry(Telemetry.ExtendSelectionByCellBelow)
public extendSelectionByCellBelow() {
// This behaves similarly to excel "Extend Selection by One Cell Above".
// The direction of the selection matters (i.e. where the active cursor)
// position is. First, it ensures that complete cells are selection.
// If so, then if active cursor is in cells below it expands the
// selection range. If the active cursor is above, it contracts the
// selection range.
const editor = this.documentManager.activeTextEditor;
if (!editor || !editor.selection) {
return;
}
const currentSelection = editor.selection;
const startEndCellIndex = this.getStartEndCellIndex(editor.selection);
if (!startEndCellIndex) {
return;
}
const isAnchorLessEqualActive = editor.selection.anchor.isBeforeOrEqual(editor.selection.active);
const cells = this.cells;
const startCellIndex = startEndCellIndex[0];
const endCellIndex = startEndCellIndex[1];
const startCell = cells[startCellIndex];
const endCell = cells[endCellIndex];
if (
!startCell.range.start.isEqual(currentSelection.start) ||
!endCell.range.end.isEqual(currentSelection.end)
) {
// full cell range not selected, first select a full cell range.
let selection: Selection;
if (isAnchorLessEqualActive) {
// active at start of start cell.
selection = new Selection(startCell.range.start, endCell.range.end);
} else {
if (startCellIndex < endCellIndex) {
// active at end of cell before endCell
selection = new Selection(cells[startCellIndex + 1].range.start, endCell.range.end);
} else {
// active at end of startCell
selection = new Selection(endCell.range.start, endCell.range.end);
}
}
editor.selection = selection;
} else {
let newCell: ICellRange | undefined;
// full cell range is selected now decide if expanding or contracting?
if (isAnchorLessEqualActive || startCellIndex === endCellIndex) {
// anchor is above active, expand selection by cell below.
if (endCellIndex < cells.length - 1) {
newCell = cells[endCellIndex + 1];
editor.selection = new Selection(startCell.range.start, newCell.range.end);
}
} else {
// anchor is below active, contract selection by cell above.
if (startCellIndex < endCellIndex) {
newCell = cells[startCellIndex + 1];
editor.selection = new Selection(endCell.range.end, newCell.range.start);
}
}
if (newCell) {
editor.revealRange(newCell.range, TextEditorRevealType.Default);
}
}
}
@captureTelemetry(Telemetry.MoveCellsUp)
public async moveCellsUp(): Promise<void> {
await this.moveCellsDirection(true);
}
@captureTelemetry(Telemetry.MoveCellsDown)
public async moveCellsDown(): Promise<void> {
await this.moveCellsDirection(false);
}
@captureTelemetry(Telemetry.ChangeCellToMarkdown)
public changeCellToMarkdown() {
this.applyToCells((editor, cell, _) => {
return this.changeCellTo(editor, cell, 'markdown');
});
}
@captureTelemetry(Telemetry.ChangeCellToCode)
public changeCellToCode() {
this.applyToCells((editor, cell, _) => {
return this.changeCellTo(editor, cell, 'code');
});
}
@captureTelemetry(Telemetry.GotoNextCellInFile)
public gotoNextCell() {
const editor = this.documentManager.activeTextEditor;
if (!editor || !editor.selection) {
return;
}
const currentSelection = editor.selection;
const currentRunCellLens = this.getCurrentCellLens(currentSelection.start);
const nextRunCellLens = this.getNextCellLens(currentSelection.start);
if (currentRunCellLens && nextRunCellLens) {
this.advanceToRange(nextRunCellLens.range);
}
}
@captureTelemetry(Telemetry.GotoPrevCellInFile)
public gotoPreviousCell() {
const editor = this.documentManager.activeTextEditor;
if (!editor || !editor.selection) {
return;
}
const currentSelection = editor.selection;
const currentRunCellLens = this.getCurrentCellLens(currentSelection.start);
const prevRunCellLens = this.getPreviousCellLens(currentSelection.start);
if (currentRunCellLens && prevRunCellLens) {
this.advanceToRange(prevRunCellLens.range);
}
}
private applyToCells(callback: (editor: TextEditor, cell: ICellRange, cellIndex: number) => void) {
const editor = this.documentManager.activeTextEditor;
const startEndCellIndex = this.getStartEndCellIndex(editor?.selection);
if (!editor || !startEndCellIndex) {
return;
}
const cells = this.cells;
const startIndex = startEndCellIndex[0];
const endIndex = startEndCellIndex[1];
for (let cellIndex = startIndex; cellIndex <= endIndex; cellIndex += 1) {
callback(editor, cells[cellIndex], cellIndex);
}
}
private changeCellTo(editor: TextEditor, cell: ICellRange, toCellType: nbformat.CellType) {
// change cell from code -> markdown or markdown -> code
if (toCellType === 'raw') {
throw Error('Cell Type raw not implemented');
}
// don't change cell type if already that type
if (cell.cell_type === toCellType) {
return;
}
const cellMatcher = new CellMatcher(this.configService.getSettings(editor.document.uri));
const definitionLine = editor.document.lineAt(cell.range.start.line);
const definitionText = editor.document.getText(definitionLine.range);
// new definition text
const cellMarker = this.getDefaultCellMarker(editor.document.uri);
const definitionMatch =
toCellType === 'markdown'
? cellMatcher.codeExecRegEx.exec(definitionText) // code -> markdown
: cellMatcher.markdownExecRegEx.exec(definitionText); // markdown -> code
if (!definitionMatch) {
return;
}
const definitionExtra = definitionMatch[definitionMatch.length - 1];
const newDefinitionText =
toCellType === 'markdown'
? `${cellMarker} [markdown]${definitionExtra}` // code -> markdown
: `${cellMarker}${definitionExtra}`; // markdown -> code
void editor.edit(async (editBuilder) => {
editBuilder.replace(definitionLine.range, newDefinitionText);
cell.cell_type = toCellType;
if (cell.range.start.line < cell.range.end.line) {
editor.selection = new Selection(
cell.range.start.line + 1,
0,
cell.range.end.line,
cell.range.end.character
);
// ensure all lines in markdown cell have a comment.
// these are not included in the test because it's unclear
// how TypeMoq works with them.
void commands.executeCommand('editor.action.removeCommentLine');
if (toCellType === 'markdown') {
void commands.executeCommand('editor.action.addCommentLine');
}
}
});
}
private async moveCellsDirection(directionUp: boolean): Promise<boolean> {
const editor = this.documentManager.activeTextEditor;
if (!editor || !editor.selection) {
return false;
}
const startEndCellIndex = this.getStartEndCellIndex(editor.selection);
if (!startEndCellIndex) {
return false;
}
const startCellIndex = startEndCellIndex[0];
const endCellIndex = startEndCellIndex[1];
const cells = this.cells;
const startCell = cells[startCellIndex];
const endCell = cells[endCellIndex];
if (!startCell || !endCell) {
return false;
}
const currentRange = new Range(startCell.range.start, endCell.range.end);
const relativeSelectionRange = new Range(
editor.selection.start.line - currentRange.start.line,
editor.selection.start.character,
editor.selection.end.line - currentRange.start.line,
editor.selection.end.character
);
const isActiveBeforeAnchor = editor.selection.active.isBefore(editor.selection.anchor);
let thenSetSelection: Thenable<boolean>;
if (directionUp) {
if (startCellIndex === 0) {
return false;
} else {
const aboveCell = cells[startCellIndex - 1];
const thenExchangeTextLines = this.exchangeTextLines(editor, aboveCell.range, currentRange);
thenSetSelection = thenExchangeTextLines.then((isEditSuccessful) => {
if (isEditSuccessful) {
editor.selection = new Selection(
aboveCell.range.start.line + relativeSelectionRange.start.line,
relativeSelectionRange.start.character,
aboveCell.range.start.line + relativeSelectionRange.end.line,
relativeSelectionRange.end.character
);
}
return isEditSuccessful;
});
}
} else {
if (endCellIndex === cells.length - 1) {
return false;
} else {
const belowCell = cells[endCellIndex + 1];
const thenExchangeTextLines = this.exchangeTextLines(editor, currentRange, belowCell.range);
const belowCellLineLength = belowCell.range.end.line - belowCell.range.start.line;
const aboveCellLineLength = currentRange.end.line - currentRange.start.line;
const diffCellLineLength = belowCellLineLength - aboveCellLineLength;
thenSetSelection = thenExchangeTextLines.then((isEditSuccessful) => {
if (isEditSuccessful) {
editor.selection = new Selection(
belowCell.range.start.line + diffCellLineLength + relativeSelectionRange.start.line,
relativeSelectionRange.start.character,
belowCell.range.start.line + diffCellLineLength + relativeSelectionRange.end.line,
relativeSelectionRange.end.character
);
}
return isEditSuccessful;
});
}
}
return thenSetSelection.then((isEditSuccessful) => {
if (isEditSuccessful && isActiveBeforeAnchor) {
editor.selection = new Selection(editor.selection.active, editor.selection.anchor);
}
return true;
});
}
private exchangeTextLines(editor: TextEditor, aboveRange: Range, belowRange: Range): Thenable<boolean> {
const aboveStartLine = aboveRange.start.line;
const aboveEndLine = aboveRange.end.line;
const belowStartLine = belowRange.start.line;
const belowEndLine = belowRange.end.line;
if (aboveEndLine >= belowStartLine) {
throw RangeError(`Above lines must be fully above not ${aboveEndLine} <= ${belowStartLine}`);
}
const above = new Range(
aboveStartLine,
0,
aboveEndLine,
editor.document.lineAt(aboveEndLine).range.end.character
);
const aboveText = editor.document.getText(above);
const below = new Range(
belowStartLine,
0,
belowEndLine,
editor.document.lineAt(belowEndLine).range.end.character
);
const belowText = editor.document.getText(below);
let betweenText = '';
if (aboveEndLine + 1 < belowStartLine) {
const betweenStatLine = aboveEndLine + 1;
const betweenEndLine = belowStartLine - 1;
const between = new Range(
betweenStatLine,
0,
betweenEndLine,
editor.document.lineAt(betweenEndLine).range.end.character
);
betweenText = `${editor.document.getText(between)}\n`;
}
const newText = `${belowText}\n${betweenText}${aboveText}`;
const newRange = new Range(above.start, below.end);
return editor.edit((editBuilder) => {
editBuilder.replace(newRange, newText);
this.codeLensUpdatedEvent.fire();
});
}
private getStartEndCells(selection: Selection): ICellRange[] | undefined {
const startEndCellIndex = this.getStartEndCellIndex(selection);
if (startEndCellIndex) {
const startCell = this.getCellFromIndex(startEndCellIndex[0]);
const endCell = this.getCellFromIndex(startEndCellIndex[1]);
return [startCell, endCell];
}
}
private getStartEndCellIndex(selection?: Selection): number[] | undefined {
if (!selection) {
return undefined;
}
let startCellIndex = this.getCellIndex(selection.start);
let endCellIndex = startCellIndex;
// handle if the selection is the same line, hence same cell
if (selection.start.line !== selection.end.line) {
endCellIndex = this.getCellIndex(selection.end);
}
// handle when selection is above the top most cell
if (startCellIndex === -1) {
if (endCellIndex === -1) {
return undefined;
} else {
// selected a range above the first cell.
startCellIndex = 0;
const startCell = this.getCellFromIndex(0);
if (selection.start.line > startCell.range.start.line) {
throw RangeError(
`Should not be able to pick a range with an end in a cell and start after a cell. ${selection.start.line} > ${startCell.range.end.line}`
);
}
}
}
if (startCellIndex >= 0 && endCellIndex >= 0) {
return [startCellIndex, endCellIndex];
}
}
private insertCell(editor: TextEditor, line: number) {
// insertCell
//
// Inserts a cell at current line defined as two new lines and then
// moves cursor to within the cell.
// ```
// # %%
//
// ```
//
const cellDelineator = this.getDefaultCellMarker(editor.document.uri);
let newCell = `${cellDelineator}\n\n`;
if (line >= editor.document.lineCount) {
newCell = `\n${cellDelineator}\n`;
}
const cellStartPosition = new Position(line, 0);
const newCursorPosition = new Position(line + 1, 0);
void editor.edit((editBuilder) => {
editBuilder.insert(cellStartPosition, newCell);
this.codeLensUpdatedEvent.fire();
});
editor.selection = new Selection(newCursorPosition, newCursorPosition);
}
private getDefaultCellMarker(resource: Resource): string {
return this.configService.getSettings(resource).defaultCellMarker || Identifiers.DefaultCodeCellMarker;
}
private onCodeLensFactoryUpdated(): void {
// Update our code lenses.
if (this.document) {
this.codeLenses = this.codeLensFactory.createCodeLenses(this.document);
this.cells = this.codeLensFactory.getCellRanges(this.document);
}
this.codeLensUpdatedEvent.fire();
}
private onDocumentClosed(doc: TextDocument): void {
if (this.document && this.fs.arePathsSame(doc.uri, this.document.uri)) {
this.codeLensUpdatedEvent.dispose();
this.closeDocumentDisposable?.dispose(); // NOSONAR
this.updateRequiredDisposable?.dispose(); // NOSONAR
}
}
private async addCode(
code: string,
file: Uri,
line: number,
editor?: TextEditor,
debug?: boolean
): Promise<boolean> {
let result = false;
try {
const stopWatch = new StopWatch();
const activeInteractiveWindow = await this.interactiveWindowProvider.getOrCreate(file);
if (debug) {
result = await activeInteractiveWindow.debugCode(code, file, line, editor);
} else {
result = await activeInteractiveWindow.addCode(code, file, line, editor);
}