1
+ import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar' ;
2
+ import { CompositeTreeNode } from '@theia/core/lib/browser/tree' ;
3
+ import { codicon } from '@theia/core/lib/browser/widgets/widget' ;
4
+ import {
5
+ Disposable ,
6
+ DisposableCollection ,
7
+ } from '@theia/core/lib/common/disposable' ;
8
+ import { Emitter } from '@theia/core/lib/common/event' ;
9
+ import { nls } from '@theia/core/lib/common/nls' ;
10
+ import { inject , injectable } from '@theia/core/shared/inversify' ;
11
+ import type { AuthenticationSession } from '../../node/auth/types' ;
12
+ import { AuthenticationClientService } from '../auth/authentication-client-service' ;
13
+ import { CreateApi } from '../create/create-api' ;
14
+ import { CreateUri } from '../create/create-uri' ;
15
+ import { Create } from '../create/typings' ;
16
+ import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog' ;
17
+ import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree' ;
18
+ import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model' ;
19
+ import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget' ;
20
+ import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands' ;
21
+ import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget' ;
22
+ import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution' ;
23
+ import { Command , CommandRegistry , Contribution , URI } from './contribution' ;
24
+
25
+ interface Context {
26
+ treeModel : CloudSketchbookTreeModel | undefined ;
27
+ session : AuthenticationSession | undefined ;
28
+ }
29
+ namespace Context {
30
+ export function isValid ( context : Context ) : context is Context & {
31
+ treeModel : CloudSketchbookTreeModel ;
Collapse comment Comment on line R31
invert the isValid condition -> Context must be "Required
" by default, the field should be Partial<Context>
Code has comments. Press enter to view.
32
+ session : AuthenticationSession ;
33
+ } {
34
+ return ! ! context . session && ! ! context . treeModel ;
35
+ }
36
+ }
37
+
38
+ @injectable ( )
39
+ export class NewCloudSketch extends Contribution {
40
+ @inject ( CreateApi )
41
+ private readonly createApi : CreateApi ;
42
+ @inject ( SketchbookWidgetContribution )
43
+ private readonly sketchbookWidgetContribution : SketchbookWidgetContribution ;
44
+ @inject ( AuthenticationClientService )
45
+ private readonly authenticationService : AuthenticationClientService ;
46
+
47
+ private toDisposeOnNewTreeModel : Disposable | undefined ;
48
+ private readonly context : Context = {
49
+ treeModel : undefined ,
50
+ session : undefined ,
51
+ } ;
52
+ private readonly onDidChangeEmitter = new Emitter < void > ( ) ;
53
+ private readonly toDisposeOnStop = new DisposableCollection (
54
+ this . onDidChangeEmitter
55
+ ) ;
56
+
57
+ override onReady ( ) : void {
58
+ const handleCurrentTreeDidChange = ( widget : SketchbookWidget ) => {
59
+ this . toDisposeOnStop . push (
60
+ widget . onCurrentTreeDidChange ( ( ) => this . onDidChangeEmitter . fire ( ) )
61
+ ) ;
62
+ const treeWidget = widget . getTreeWidget ( ) ;
63
+ if ( treeWidget instanceof CloudSketchbookTreeWidget ) {
64
+ this . onDidChangeEmitter . fire ( ) ;
65
+ }
66
+ } ;
67
+ const widget = this . sketchbookWidgetContribution . tryGetWidget ( ) ;
68
+ if ( widget ) {
69
+ handleCurrentTreeDidChange ( widget ) ;
70
+ } else {
71
+ this . sketchbookWidgetContribution . widget . then ( handleCurrentTreeDidChange ) ;
72
+ }
73
+
74
+ const handleSessionDidChange = (
75
+ session : AuthenticationSession | undefined
76
+ ) => {
77
+ this . context . session = session ;
78
+ this . onDidChangeEmitter . fire ( ) ;
79
+ } ;
80
+ this . toDisposeOnStop . push (
81
+ this . authenticationService . onSessionDidChange ( handleSessionDidChange )
82
+ ) ;
83
+ handleSessionDidChange ( this . authenticationService . session ) ;
84
+ }
85
+
86
+ onStop ( ) : void {
87
+ this . toDisposeOnStop . dispose ( ) ;
88
+ if ( this . toDisposeOnNewTreeModel ) {
89
+ this . toDisposeOnNewTreeModel . dispose ( ) ;
90
+ }
91
+ }
92
+
93
+ override registerCommands ( registry : CommandRegistry ) : void {
94
+ registry . registerCommand ( NewCloudSketch . Commands . CREATE_SKETCH , {
95
+ execute : ( ) => this . createNewSketch ( ) ,
96
+ isEnabled : ( ) => Context . isValid ( this . context ) ,
97
+ } ) ;
98
+ registry . registerCommand ( NewCloudSketch . Commands . CREATE_SKETCH_TOOLBAR , {
99
+ execute : ( ) =>
100
+ this . commandService . executeCommand (
101
+ NewCloudSketch . Commands . CREATE_SKETCH . id
102
+ ) ,
103
+ isVisible : ( arg : unknown ) => {
104
+ let treeModel : CloudSketchbookTreeModel | undefined = undefined ;
105
+ if ( arg instanceof SketchbookWidget ) {
106
+ treeModel = this . treeModelOf ( arg ) ;
107
+ if ( treeModel && this . context . treeModel !== treeModel ) {
108
+ this . context . treeModel = treeModel ;
109
+ if ( this . toDisposeOnNewTreeModel ) {
110
+ this . toDisposeOnNewTreeModel . dispose ( ) ;
111
+ this . toDisposeOnNewTreeModel = this . context . treeModel . onChanged (
112
+ ( ) => this . onDidChangeEmitter . fire ( )
113
+ ) ;
114
+ }
115
+ }
116
+ return Context . isValid ( this . context ) && ! ! treeModel ;
117
+ }
118
+ return false ;
119
+ } ,
120
+ } ) ;
121
+ }
122
+
123
+ override registerToolbarItems ( registry : TabBarToolbarRegistry ) : void {
124
+ registry . registerItem ( {
125
+ id : NewCloudSketch . Commands . CREATE_SKETCH_TOOLBAR . id ,
126
+ command : NewCloudSketch . Commands . CREATE_SKETCH_TOOLBAR . id ,
Collapse comment Comment on line R126
Code has comments. Press enter to view.
127
+ tooltip : NewCloudSketch . Commands . CREATE_SKETCH_TOOLBAR . label ,
128
+ onDidChange : this . onDidChangeEmitter . event ,
129
+ } ) ;
130
+ }
131
+
132
+ private treeModelOf (
Collapse comment Comment on line R132
Code has comments. Press enter to view.
133
+ widget : SketchbookWidget
134
+ ) : CloudSketchbookTreeModel | undefined {
135
+ const treeWidget = widget . getTreeWidget ( ) ;
136
+ if ( treeWidget instanceof CloudSketchbookTreeWidget ) {
137
+ const model = treeWidget . model ;
138
+ if ( model instanceof CloudSketchbookTreeModel ) {
139
+ return model ;
140
+ }
141
+ }
142
+ return undefined ;
143
+ }
144
+
145
+ private async createNewSketch (
146
+ initialValue ?: string | undefined
147
+ ) : Promise < URI | undefined > {
148
+ if ( ! Context . isValid ( this . context ) ) {
149
+ return undefined ;
150
+ }
151
+ const newSketchName = await this . newSketchName ( initialValue ) ;
152
+ if ( ! newSketchName ) {
153
+ return undefined ;
154
+ }
155
+ let result : Create . Sketch | undefined | 'conflict' ;
156
+ try {
157
+ result = await this . createApi . createSketch ( newSketchName ) ;
158
+ } catch ( err ) {
159
+ if ( isConflict ( err ) ) {
160
+ result = 'conflict' ;
161
+ } else {
162
+ throw err ;
163
+ }
164
+ } finally {
165
+ if ( result ) {
166
+ await this . context . treeModel . updateRoot ( ) ;
167
+ await this . context . treeModel . refresh ( ) ;
168
+ }
169
+ }
170
+
171
+ if ( result === 'conflict' ) {
172
+ return this . createNewSketch ( newSketchName ) ;
173
+ }
174
+
175
+ if ( result ) {
176
+ const newSketch = result ;
177
+ const treeModel = this . context . treeModel ;
178
+ const yes = nls . localize ( 'vscode/extensionsUtils/yes' , 'Yes' ) ;
179
+ this . messageService
180
+ . info (
181
+ nls . localize (
182
+ 'arduino/newCloudSketch/openNewSketch' ,
183
+ 'Do you want to pull the new remote sketch {0} and open it in a new window?' ,
184
+ newSketchName
185
+ ) ,
186
+ yes
187
+ )
188
+ . then ( async ( answer ) => {
189
+ if ( answer === yes ) {
190
+ const node = treeModel . getNode (
191
+ CreateUri . toUri ( newSketch ) . path . toString ( )
192
+ ) ;
193
+ if ( ! node ) {
194
+ return ;
195
+ }
196
+ if ( CloudSketchbookTree . CloudSketchDirNode . is ( node ) ) {
197
+ try {
198
+ await treeModel . sketchbookTree ( ) . pull ( { node } ) ;
199
+ } catch ( err ) {
200
+ if ( isNotFound ( err ) ) {
201
+ await treeModel . updateRoot ( ) ;
202
+ await treeModel . refresh ( ) ;
203
+ this . messageService . error (
204
+ nls . localize (
205
+ 'arduino/newCloudSketch/notFound' ,
206
+ `Could not pull the remote sketch {0}. It does not exist.` ,
207
+ newSketchName
208
+ )
209
+ ) ;
210
+ return ;
211
+ }
212
+ throw err ;
213
+ }
214
+ return this . commandService . executeCommand (
215
+ SketchbookCommands . OPEN_NEW_WINDOW . id ,
216
+ { node }
217
+ ) ;
218
+ }
219
+ }
220
+ } ) ;
221
+ }
222
+ return undefined ;
223
+ }
224
+
225
+ private async newSketchName (
226
+ initialValue ?: string | undefined
227
+ ) : Promise < string | undefined > {
228
+ const rootNode = this . rootNode ( ) ;
229
+ if ( ! rootNode ) {
230
+ return undefined ;
231
+ }
232
+ const existingNames = rootNode . children
233
+ . filter ( CloudSketchbookTree . CloudSketchDirNode . is )
234
+ . map ( ( { fileStat } ) => fileStat . name ) ;
235
+ return new WorkspaceInputDialog (
236
+ {
237
+ title : nls . localize (
238
+ 'arduino/newCloudSketch/newSketchTitle' ,
239
+ 'Name of a new remote sketch'
240
+ ) ,
241
+ parentUri : CreateUri . root ,
242
+ initialValue,
243
+ validate : ( input ) => {
244
+ if ( existingNames . includes ( input ) ) {
245
+ return nls . localize (
246
+ 'arduino/newCloudSketch/sketchAlreadyExists' ,
247
+ "Remote sketch '{0}' already exists." ,
248
+ input
249
+ ) ;
250
+ }
251
+ // This is how https://create.arduino.cc/editor/ works when renaming a sketch.
252
+ if ( / ^ [ 0 - 9 a - z A - Z _ ] { 1 , 36 } $ / . test ( input ) ) {
253
+ return '' ;
254
+ }
255
+ return nls . localize (
256
+ 'arduino/newCloudSketch/invalidSketchName' ,
257
+ 'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
258
+ ) ;
259
+ } ,
260
+ } ,
261
+ this . labelProvider
262
+ ) . open ( ) ;
263
+ }
264
+
265
+ private rootNode ( ) : CompositeTreeNode | undefined {
266
+ if ( ! Context . isValid ( this . context ) ) {
267
+ return undefined ;
268
+ }
269
+ const { treeModel } = this . context ;
270
+ return CompositeTreeNode . is ( treeModel . root ) ? treeModel . root : undefined ;
271
+ }
272
+ }
273
+ export namespace NewCloudSketch {
274
+ export namespace Commands {
275
+ export const CREATE_SKETCH = Command . toLocalizedCommand (
276
+ {
277
+ id : 'arduino-cloud-sketchbook--create-sketch' ,
278
+ label : 'New Remote Sketch...' ,
279
+ category : 'Arduino' ,
280
+ } ,
281
+ 'arduino/newCloudSketch/createSketch'
282
+ ) as Command & { label : string } ;
283
+ export const CREATE_SKETCH_TOOLBAR : Command & { label : string } = {
284
+ ...CREATE_SKETCH ,
285
+ id : `${ CREATE_SKETCH . id } -toolbar` ,
286
+ iconClass : codicon ( 'new-folder' ) ,
287
+ } ;
288
+ }
289
+ }
290
+
291
+ function isConflict ( err : unknown ) : boolean {
292
+ return isErrorWithStatusOf ( err , 409 ) ;
293
+ }
294
+ function isNotFound ( err : unknown ) : boolean {
295
+ return isErrorWithStatusOf ( err , 404 ) ;
296
+ }
297
+ function isErrorWithStatusOf (
298
+ err : unknown ,
299
+ status : number
300
+ ) : err is Error & { status : number } {
301
+ if ( err instanceof Error ) {
302
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
303
+ const object = err as any ;
304
+ return 'status' in object && object . status === status ;
305
+ }
306
+ return false ;
307
+ }
0 commit comments