@@ -2,16 +2,18 @@ import { injectable, inject } from 'inversify';
2
2
import * as os from 'os' ;
3
3
import * as temp from 'temp' ;
4
4
import * as path from 'path' ;
5
+ import * as nsfw from 'nsfw' ;
5
6
import { ncp } from 'ncp' ;
6
7
import { Stats } from 'fs' ;
7
8
import * as fs from './fs-extra' ;
8
9
import URI from '@theia/core/lib/common/uri' ;
9
10
import { FileUri } from '@theia/core/lib/node' ;
11
+ import { Deferred } from '@theia/core/lib/common/promise-util' ;
10
12
import { isWindows } from '@theia/core/lib/common/os' ;
11
13
import { ConfigService } from '../common/protocol/config-service' ;
12
14
import { SketchesService , Sketch } from '../common/protocol/sketches-service' ;
13
15
import { firstToLowerCase } from '../common/utils' ;
14
-
16
+ import { NotificationServiceServerImpl } from './notification-service-server' ;
15
17
16
18
// As currently implemented on Linux,
17
19
// 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 {
28
30
@inject ( ConfigService )
29
31
protected readonly configService : ConfigService ;
30
32
33
+ @inject ( NotificationServiceServerImpl )
34
+ protected readonly notificationService : NotificationServiceServerImpl ;
35
+
31
36
async getSketches ( uri ?: string ) : Promise < Sketch [ ] > {
32
- const sketches : Array < Sketch & { mtimeMs : number } > = [ ] ;
33
37
let fsPath : undefined | string ;
34
38
if ( ! uri ) {
35
39
const { sketchDirUri } = await this . configService . getConfiguration ( ) ;
@@ -43,9 +47,62 @@ export class SketchesServiceImpl implements SketchesService {
43
47
if ( ! fs . existsSync ( fsPath ) ) {
44
48
return [ ] ;
45
49
}
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 ) ;
49
106
if ( await this . isSketchFolder ( FileUri . create ( filePath ) . toString ( ) ) ) {
50
107
try {
51
108
const stat = await fs . stat ( filePath ) ;
@@ -59,7 +116,84 @@ export class SketchesServiceImpl implements SketchesService {
59
116
}
60
117
}
61
118
}
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 ;
63
197
}
64
198
65
199
/**
0 commit comments