@@ -2,16 +2,18 @@ import { injectable, inject } from 'inversify';
22import * as os from 'os' ;
33import * as temp from 'temp' ;
44import * as path from 'path' ;
5+ import * as nsfw from 'nsfw' ;
56import { ncp } from 'ncp' ;
67import { Stats } from 'fs' ;
78import * as fs from './fs-extra' ;
89import URI from '@theia/core/lib/common/uri' ;
910import { FileUri } from '@theia/core/lib/node' ;
11+ import { Deferred } from '@theia/core/lib/common/promise-util' ;
1012import { isWindows } from '@theia/core/lib/common/os' ;
1113import { ConfigService } from '../common/protocol/config-service' ;
1214import { SketchesService , Sketch } from '../common/protocol/sketches-service' ;
1315import { firstToLowerCase } from '../common/utils' ;
14-
16+ import { NotificationServiceServerImpl } from './notification-service-server' ;
1517
1618// As currently implemented on Linux,
1719// the maximum number of symbolic links that will be followed while resolving a pathname is 40
@@ -28,8 +30,10 @@ export class SketchesServiceImpl implements SketchesService {
2830 @inject ( ConfigService )
2931 protected readonly configService : ConfigService ;
3032
33+ @inject ( NotificationServiceServerImpl )
34+ protected readonly notificationService : NotificationServiceServerImpl ;
35+
3136 async getSketches ( uri ?: string ) : Promise < Sketch [ ] > {
32- const sketches : Array < Sketch & { mtimeMs : number } > = [ ] ;
3337 let fsPath : undefined | string ;
3438 if ( ! uri ) {
3539 const { sketchDirUri } = await this . configService . getConfiguration ( ) ;
@@ -43,9 +47,62 @@ export class SketchesServiceImpl implements SketchesService {
4347 if ( ! fs . existsSync ( fsPath ) ) {
4448 return [ ] ;
4549 }
46- const fileNames = await fs . readdir ( fsPath ) ;
47- for ( const fileName of fileNames ) {
48- const filePath = path . join ( fsPath , fileName ) ;
50+ const stat = await fs . stat ( fsPath ) ;
51+ if ( ! stat . isDirectory ( ) ) {
52+ return [ ] ;
53+ }
54+ return this . doGetSketches ( fsPath ) ;
55+ }
56+
57+ /**
58+ * Dev note: The keys are filesystem paths, not URI strings.
59+ */
60+ private sketchbooks = new Map < string , Sketch [ ] | Deferred < Sketch [ ] > > ( ) ;
61+ private fireSoonHandle ?: NodeJS . Timer ;
62+ private bufferedSketchbookEvents : { type : 'created' | 'removed' , sketch : Sketch } [ ] = [ ] ;
63+
64+ private fireSoon ( type : 'created' | 'removed' , sketch : Sketch ) : void {
65+ this . bufferedSketchbookEvents . push ( { type, sketch } ) ;
66+
67+ if ( this . fireSoonHandle ) {
68+ clearTimeout ( this . fireSoonHandle ) ;
69+ }
70+
71+ this . fireSoonHandle = setTimeout ( ( ) => {
72+ const event : { created : Sketch [ ] , removed : Sketch [ ] } = {
73+ created : [ ] ,
74+ removed : [ ]
75+ } ;
76+ for ( const { type, sketch } of this . bufferedSketchbookEvents ) {
77+ if ( type === 'created' ) {
78+ event . created . push ( sketch ) ;
79+ } else {
80+ event . removed . push ( sketch ) ;
81+ }
82+ }
83+ this . notificationService . notifySketchbookChanged ( event ) ;
84+ this . bufferedSketchbookEvents . length = 0 ;
85+ } , 100 ) ;
86+ }
87+
88+ /**
89+ * Assumes the `fsPath` points to an existing directory.
90+ */
91+ private async doGetSketches ( sketchbookPath : string ) : Promise < Sketch [ ] > {
92+ const resolvedSketches = this . sketchbooks . get ( sketchbookPath ) ;
93+ if ( resolvedSketches ) {
94+ if ( Array . isArray ( resolvedSketches ) ) {
95+ return resolvedSketches ;
96+ }
97+ return resolvedSketches . promise ;
98+ }
99+
100+ const deferred = new Deferred < Sketch [ ] > ( ) ;
101+ this . sketchbooks . set ( sketchbookPath , deferred ) ;
102+ const sketches : Array < Sketch & { mtimeMs : number } > = [ ] ;
103+ const filenames = await fs . readdir ( sketchbookPath ) ;
104+ for ( const fileName of filenames ) {
105+ const filePath = path . join ( sketchbookPath , fileName ) ;
49106 if ( await this . isSketchFolder ( FileUri . create ( filePath ) . toString ( ) ) ) {
50107 try {
51108 const stat = await fs . stat ( filePath ) ;
@@ -59,7 +116,84 @@ export class SketchesServiceImpl implements SketchesService {
59116 }
60117 }
61118 }
62- return sketches . sort ( ( left , right ) => right . mtimeMs - left . mtimeMs ) ;
119+ sketches . sort ( ( left , right ) => right . mtimeMs - left . mtimeMs ) ;
120+ const deleteSketch = ( toDelete : Sketch & { mtimeMs : number } ) => {
121+ const index = sketches . indexOf ( toDelete ) ;
122+ if ( index !== - 1 ) {
123+ console . log ( `Sketch '${ toDelete . name } ' was removed from sketchbook '${ sketchbookPath } '.` ) ;
124+ sketches . splice ( index , 1 ) ;
125+ sketches . sort ( ( left , right ) => right . mtimeMs - left . mtimeMs ) ;
126+ this . fireSoon ( 'removed' , toDelete ) ;
127+ }
128+ } ;
129+ const createSketch = async ( path : string ) => {
130+ try {
131+ const [ stat , sketch ] = await Promise . all ( [
132+ fs . stat ( path ) ,
133+ this . loadSketch ( path )
134+ ] ) ;
135+ console . log ( `New sketch '${ sketch . name } ' was crated in sketchbook '${ sketchbookPath } '.` ) ;
136+ sketches . push ( { ...sketch , mtimeMs : stat . mtimeMs } ) ;
137+ sketches . sort ( ( left , right ) => right . mtimeMs - left . mtimeMs ) ;
138+ this . fireSoon ( 'created' , sketch ) ;
139+ } catch { }
140+ } ;
141+ const watcher = await nsfw ( sketchbookPath , async ( events : any ) => {
142+ // We track `.ino` files changes only.
143+ for ( const event of events ) {
144+ switch ( event . action ) {
145+ case nsfw . ActionType . CREATED :
146+ if ( event . file . endsWith ( '.ino' ) && path . join ( event . directory , '..' ) === sketchbookPath && event . file === `${ path . basename ( event . directory ) } .ino` ) {
147+ createSketch ( event . directory ) ;
148+ }
149+ break ;
150+ case nsfw . ActionType . DELETED :
151+ let sketch : Sketch & { mtimeMs : number } | undefined = undefined
152+ // Deleting the `ino` file.
153+ if ( event . file . endsWith ( '.ino' ) && path . join ( event . directory , '..' ) === sketchbookPath && event . file === `${ path . basename ( event . directory ) } .ino` ) {
154+ sketch = sketches . find ( sketch => FileUri . fsPath ( sketch . uri ) === event . directory ) ;
155+ } else if ( event . directory === sketchbookPath ) { // Deleting the sketch (or any folder folder in the sketchbook).
156+ sketch = sketches . find ( sketch => FileUri . fsPath ( sketch . uri ) === path . join ( event . directory , event . file ) ) ;
157+ }
158+ if ( sketch ) {
159+ deleteSketch ( sketch ) ;
160+ }
161+ break ;
162+ case nsfw . ActionType . RENAMED :
163+ let sketchToDelete : Sketch & { mtimeMs : number } | undefined = undefined
164+ // When renaming with the Java IDE we got an event where `directory` is the sketchbook and `oldFile` is the sketch.
165+ if ( event . directory === sketchbookPath ) {
166+ sketchToDelete = sketches . find ( sketch => FileUri . fsPath ( sketch . uri ) === path . join ( event . directory , event . oldFile ) ) ;
167+ }
168+
169+ if ( sketchToDelete ) {
170+ deleteSketch ( sketchToDelete ) ;
171+ } else {
172+ // If it's not a deletion, check for creation. The `directory` is the new sketch and the `newFile` is the new `ino` file.
173+ // tslint:disable-next-line:max-line-length
174+ if ( event . newFile . endsWith ( '.ino' ) && path . join ( event . directory , '..' ) === sketchbookPath && event . newFile === `${ path . basename ( event . directory ) } .ino` ) {
175+ createSketch ( event . directory ) ;
176+ } else {
177+ // When renaming the `ino` file directly on the filesystem. The `directory` is the sketch and `newFile` and `oldFile` is the `ino` file.
178+ // tslint:disable-next-line:max-line-length
179+ if ( event . oldFile . endsWith ( '.ino' ) && path . join ( event . directory , '..' ) === sketchbookPath && event . oldFile === `${ path . basename ( event . directory ) } .ino` ) {
180+ sketchToDelete = sketches . find ( sketch => FileUri . fsPath ( sketch . uri ) === event . directory , event . oldFile ) ;
181+ }
182+ if ( sketchToDelete ) {
183+ deleteSketch ( sketchToDelete ) ;
184+ } else if ( event . directory === sketchbookPath ) {
185+ createSketch ( path . join ( event . directory , event . newFile ) ) ;
186+ }
187+ }
188+ }
189+ break ;
190+ }
191+ }
192+ } ) ;
193+ await watcher . start ( ) ;
194+ deferred . resolve ( sketches ) ;
195+ this . sketchbooks . set ( sketchbookPath , sketches ) ;
196+ return sketches ;
63197 }
64198
65199 /**
0 commit comments