22// Licensed under the MIT License.
33import * as path from 'path' ;
44import * as uuid from 'uuid/v4' ;
5- import { Disposable , DocumentSelector , EndOfLine , Position , Range , TextDocument , TextLine , Uri } from 'vscode' ;
5+ import {
6+ Disposable ,
7+ DocumentSelector ,
8+ EndOfLine ,
9+ Event ,
10+ EventEmitter ,
11+ Position ,
12+ Location ,
13+ Range ,
14+ TextDocument ,
15+ TextDocumentChangeEvent ,
16+ TextLine ,
17+ Uri ,
18+ } from 'vscode' ;
19+ import { isEqual } from 'lodash' ;
620import { NotebookConcatTextDocument , NotebookCell , NotebookDocument } from 'vscode-proposed' ;
721import { IVSCodeNotebook } from '../../common/application/types' ;
822import { IDisposable } from '../../common/types' ;
@@ -54,6 +68,10 @@ export class NotebookConcatDocument implements TextDocument, IDisposable {
5468 return this . notebook . cells . map ( ( c ) => c . document . lineCount ) . reduce ( ( p , c ) => p + c ) ;
5569 }
5670
71+ public get onCellsChanged ( ) : Event < TextDocumentChangeEvent > {
72+ return this . onCellsChangedEmitter . event ;
73+ }
74+
5775 public firedOpen = false ;
5876
5977 public firedClose = false ;
@@ -68,6 +86,10 @@ export class NotebookConcatDocument implements TextDocument, IDisposable {
6886
6987 private onDidChangeSubscription : Disposable ;
7088
89+ private cellTracking : { uri : Uri ; lineCount : number ; length : number } [ ] = [ ] ;
90+
91+ private onCellsChangedEmitter = new EventEmitter < TextDocumentChangeEvent > ( ) ;
92+
7193 constructor ( public notebook : NotebookDocument , notebookApi : IVSCodeNotebook , selector : DocumentSelector ) {
7294 const dir = path . dirname ( notebook . uri . fsPath ) ;
7395 // Note: Has to be different than the prefix for old notebook editor (HiddenFileFormat) so
@@ -76,6 +98,7 @@ export class NotebookConcatDocument implements TextDocument, IDisposable {
7698 this . dummyUri = Uri . file ( this . dummyFilePath ) ;
7799 this . concatDocument = notebookApi . createConcatTextDocument ( notebook , selector ) ;
78100 this . onDidChangeSubscription = this . concatDocument . onDidChange ( this . onDidChange , this ) ;
101+ this . updateCellTracking ( ) ;
79102 }
80103
81104 public dispose ( ) : void {
@@ -139,7 +162,133 @@ export class NotebookConcatDocument implements TextDocument, IDisposable {
139162 return this . notebook . cells . find ( ( c ) => c . uri === location . uri ) ;
140163 }
141164
165+ private updateCellTracking ( ) {
166+ this . cellTracking = [ ] ;
167+ this . notebook . cells . forEach ( ( c ) => {
168+ // Compute end position from number of lines in a cell
169+ const cellText = c . document . getText ( ) ;
170+ const lines = cellText . splitLines ( { trim : false } ) ;
171+
172+ this . cellTracking . push ( {
173+ uri : c . uri ,
174+ length : cellText . length + 1 , // \n is included concat length
175+ lineCount : lines . length ,
176+ } ) ;
177+ } ) ;
178+ }
179+
142180 private onDidChange ( ) {
143181 this . _version += 1 ;
182+ const newUris = this . notebook . cells . map ( ( c ) => c . uri . toString ( ) ) ;
183+ const oldUris = this . cellTracking . map ( ( c ) => c . uri . toString ( ) ) ;
184+
185+ // See if number of cells or cell positions changed
186+ if ( this . cellTracking . length < this . notebook . cells . length ) {
187+ this . raiseCellInsertions ( oldUris ) ;
188+ } else if ( this . cellTracking . length > this . notebook . cells . length ) {
189+ this . raiseCellDeletions ( newUris , oldUris ) ;
190+ } else if ( ! isEqual ( oldUris , newUris ) ) {
191+ this . raiseCellMovement ( ) ;
192+ }
193+ this . updateCellTracking ( ) ;
194+ }
195+
196+ private getPositionOfCell ( cellUri : Uri ) : Position {
197+ return this . concatDocument . positionAt ( new Location ( cellUri , new Position ( 0 , 0 ) ) ) ;
198+ }
199+
200+ public getEndPosition ( ) : Position {
201+ if ( this . notebook . cells . length > 0 ) {
202+ const finalCell = this . notebook . cells [ this . notebook . cells . length - 1 ] ;
203+ const start = this . getPositionOfCell ( finalCell . uri ) ;
204+ const lines = finalCell . document . getText ( ) . splitLines ( { trim : false } ) ;
205+ return new Position ( start . line + lines . length , 0 ) ;
206+ }
207+ return new Position ( 0 , 0 ) ;
208+ }
209+
210+ private raiseCellInsertions ( oldUris : string [ ] ) {
211+ // One or more cells were added. Add a change event for each
212+ const insertions = this . notebook . cells . filter ( ( c ) => ! oldUris . includes ( c . uri . toString ( ) ) ) ;
213+
214+ const changes = insertions . map ( ( insertion ) => {
215+ // Figure out the position of the item. This is where we're inserting the cell
216+ // Note: The first insertion will line up with the old cell at this position
217+ // The second or other insertions will line up with their new positions.
218+ const position = this . getPositionOfCell ( insertion . uri ) ;
219+
220+ // Text should be the contents of the new cell plus the '\n'
221+ const text = `${ insertion . document . getText ( ) } \n` ;
222+
223+ return {
224+ text,
225+ range : new Range ( position , position ) ,
226+ rangeLength : 0 ,
227+ rangeOffset : 0 ,
228+ } ;
229+ } ) ;
230+
231+ // Send all of the changes
232+ this . onCellsChangedEmitter . fire ( {
233+ document : this ,
234+ contentChanges : changes ,
235+ } ) ;
236+ }
237+
238+ private raiseCellDeletions ( newUris : string [ ] , oldUris : string [ ] ) {
239+ // cells were deleted. Figure out which ones
240+ const oldIndexes : number [ ] = [ ] ;
241+ oldUris . forEach ( ( o , i ) => {
242+ if ( ! newUris . includes ( o ) ) {
243+ oldIndexes . push ( i ) ;
244+ }
245+ } ) ;
246+ const changes = oldIndexes . map ( ( index ) => {
247+ // Figure out the position of the item in the new list
248+ const position =
249+ index < newUris . length ? this . getPositionOfCell ( this . notebook . cells [ index ] . uri ) : this . getEndPosition ( ) ;
250+
251+ // Length should be old length
252+ const { length } = this . cellTracking [ index ] ;
253+
254+ // Range should go from new position to end of old position
255+ const endPosition = new Position ( position . line + this . cellTracking [ index ] . lineCount , 0 ) ;
256+
257+ // Turn this cell into a change event.
258+ return {
259+ text : '' ,
260+ range : new Range ( position , endPosition ) ,
261+ rangeLength : length ,
262+ rangeOffset : 0 ,
263+ } ;
264+ } ) ;
265+
266+ // Send the event
267+ this . onCellsChangedEmitter . fire ( {
268+ document : this ,
269+ contentChanges : changes ,
270+ } ) ;
271+ }
272+
273+ private raiseCellMovement ( ) {
274+ // When moving, just replace everything. Simpler this way. Might this
275+ // cause unknown side effects? Don't think so.
276+ this . onCellsChangedEmitter . fire ( {
277+ document : this ,
278+ contentChanges : [
279+ {
280+ text : this . concatDocument . getText ( ) ,
281+ range : new Range (
282+ new Position ( 0 , 0 ) ,
283+ new Position (
284+ this . cellTracking . reduce ( ( p , c ) => p + c . lineCount , 0 ) ,
285+ 0 ,
286+ ) ,
287+ ) ,
288+ rangeLength : this . cellTracking . reduce ( ( p , c ) => p + c . length , 0 ) ,
289+ rangeOffset : 0 ,
290+ } ,
291+ ] ,
292+ } ) ;
144293 }
145294}
0 commit comments