1
+ import assert from 'assert' ;
2
+ import { existsSync } from 'fs' ;
1
3
import * as vscode from 'vscode' ;
2
4
import { SymbolKind } from 'vscode' ;
5
+ import { Disposable } from 'vscode-jsonrpc' ;
3
6
import { ContextClients } from './clients' ;
4
7
import { getOrAskForProgram } from './debugConfigProvider' ;
5
8
import { mainOutputChannel } from './extension' ;
6
- import { getEnclosingSymbol } from './taskProviders' ;
9
+ import { getProjectFileRelPath } from './helpers' ;
10
+ import { CustomTaskDefinition , getEnclosingSymbol } from './taskProviders' ;
7
11
8
12
export function registerCommands ( context : vscode . ExtensionContext , clients : ContextClients ) {
9
13
context . subscriptions . push (
@@ -26,6 +30,14 @@ export function registerCommands(context: vscode.ExtensionContext, clients: Cont
26
30
)
27
31
) ;
28
32
33
+ context . subscriptions . push (
34
+ vscode . commands . registerCommand ( 'ada.runMainLast' , ( ) => runMainLast ( ) )
35
+ ) ;
36
+
37
+ context . subscriptions . push (
38
+ vscode . commands . registerCommand ( 'ada.runMainAsk' , ( ) => runMainAsk ( ) )
39
+ ) ;
40
+
29
41
// This is a hidden command that gets called in the default debug
30
42
// configuration snippet that gets offered in the launch.json file.
31
43
context . subscriptions . push (
@@ -89,3 +101,222 @@ async function addSupbrogramBoxCommand() {
89
101
}
90
102
} ) ;
91
103
}
104
+
105
+ let lastUsedTaskInfo : { source : string ; name : string } | undefined ;
106
+
107
+ /**
108
+ * If a task was previously run through the commands `ada.runMainAsk` or
109
+ * `ada.runMainLast`, re-run the same task. If not, defer to {@link runMainAsk}
110
+ * to ask the User to select a task to run.
111
+ *
112
+ * @returns the TaskExecution corresponding to the task.
113
+ */
114
+ async function runMainLast ( ) {
115
+ const buildAndRunTasks = await getBuildAndRunTasks ( ) ;
116
+ if ( lastUsedTaskInfo ) {
117
+ const matchingTasks = buildAndRunTasks . filter ( matchesLastUsedTask ) ;
118
+ assert ( matchingTasks . length <= 1 ) ;
119
+ const lastTask = matchingTasks . length == 1 ? matchingTasks [ 0 ] : undefined ;
120
+ if ( lastTask ) {
121
+ return await vscode . tasks . executeTask ( lastTask ) ;
122
+ }
123
+ }
124
+
125
+ // No task was run so far, or the last one run no longer exists
126
+ return runMainAsk ( ) ;
127
+ }
128
+
129
+ /**
130
+ *
131
+ * @param t - a task
132
+ * @returns `true` if the given task matches the last executed task
133
+ */
134
+ function matchesLastUsedTask ( t : vscode . Task ) : boolean {
135
+ return t . source == lastUsedTaskInfo ?. source && t . name == lastUsedTaskInfo ?. name ;
136
+ }
137
+
138
+ /**
139
+ *
140
+ * @param task - a task
141
+ * @returns the label to be displayed to the user in the quick picker for that task
142
+ */
143
+ function getTaskLabel ( task : vscode . Task ) : string {
144
+ return isFromWorkspace ( task ) ? `(From Workspace) ${ task . name } ` : getConventionalTaskLabel ( task ) ;
145
+ }
146
+
147
+ /**
148
+ *
149
+ * @param task - a task
150
+ * @returns the label typically generated for that task by vscode. For tasks not
151
+ * defined explicitely in the workspace, this is `ada: <task name>`. For tasks
152
+ * defined in the workspace simply return the name which should already include
153
+ * the convention.
154
+ */
155
+ function getConventionalTaskLabel ( task : vscode . Task ) : string {
156
+ return isFromWorkspace ( task ) ? task . name : `${ task . source } : ${ task . name } ` ;
157
+ }
158
+
159
+ /**
160
+ *
161
+ * @param task - a task
162
+ * @returns `true` if the task is defined explicitely in the workspace's tasks.json
163
+ */
164
+ function isFromWorkspace ( task : vscode . Task ) {
165
+ return task . source == 'Workspace' ;
166
+ }
167
+
168
+ interface TaskQuickPickItem extends vscode . QuickPickItem {
169
+ task : vscode . Task ;
170
+ }
171
+
172
+ /**
173
+ * Propose to the User a list of build and run tasks, one for each main defined
174
+ * in the project.
175
+ *
176
+ * Tasks defined explicitely in the workspace are identified as such in the
177
+ * offered list and proposed first.
178
+ *
179
+ * The User can choose either to run the task as is, or click the secondary
180
+ * button to add the task to tasks.json (if not already there) and configure it
181
+ * there.
182
+ */
183
+ async function runMainAsk ( ) {
184
+ function createQuickPickItem ( task : vscode . Task ) : TaskQuickPickItem {
185
+ return {
186
+ // Mark the last used task with a leading star
187
+ label : ( matchesLastUsedTask ( task ) ? '$(star) ' : '' ) + getTaskLabel ( task ) ,
188
+ // Add a description to the last used task
189
+ description : matchesLastUsedTask ( task ) ? 'last used' : undefined ,
190
+ task : task ,
191
+ // Add a button allowing to configure the task in tasks.json
192
+ buttons : [
193
+ {
194
+ iconPath : new vscode . ThemeIcon ( 'gear' ) ,
195
+ tooltip : 'Configure task in tasks.json, e.g. to add main arguments' ,
196
+ } ,
197
+ ] ,
198
+ } ;
199
+ }
200
+ const adaTasksMain = await getBuildAndRunTasks ( ) ;
201
+
202
+ if ( adaTasksMain . length > 0 ) {
203
+ const tasksFromWorkspace = adaTasksMain . filter ( isFromWorkspace ) ;
204
+ const tasksFromExtension = adaTasksMain . filter ( ( v ) => ! isFromWorkspace ( v ) ) ;
205
+
206
+ // Propose workspace-configured tasks first
207
+ const quickPickItems : TaskQuickPickItem [ ] = tasksFromWorkspace . map ( createQuickPickItem ) ;
208
+
209
+ if ( tasksFromWorkspace . length > 0 ) {
210
+ // Use a separator between workspace tasks and implicit tasks provided by the extension
211
+ quickPickItems . push ( {
212
+ kind : vscode . QuickPickItemKind . Separator ,
213
+ label : '' ,
214
+ // Use any valid task to avoid allowing 'undefined' in the type declaration
215
+ task : adaTasksMain [ 0 ] ,
216
+ } ) ;
217
+ }
218
+
219
+ quickPickItems . push ( ...tasksFromExtension . map ( createQuickPickItem ) ) ;
220
+
221
+ // Create the quick picker
222
+ const qp = vscode . window . createQuickPick < TaskQuickPickItem > ( ) ;
223
+ qp . items = qp . items . concat ( quickPickItems ) ;
224
+
225
+ // Array for event handlers to be disposed after the quick picker is disposed
226
+ const disposables : Disposable [ ] = [ ] ;
227
+ try {
228
+ const choice : TaskQuickPickItem | undefined = await new Promise ( ( resolve ) => {
229
+ // Add event handlers to the quick picker
230
+ disposables . push (
231
+ qp . onDidChangeSelection ( ( items ) => {
232
+ // When the User selects an option, resolve the Promise
233
+ // and hide the quick picker
234
+ const item = items [ 0 ] ;
235
+ if ( item ) {
236
+ resolve ( item ) ;
237
+ qp . hide ( ) ;
238
+ }
239
+ } ) ,
240
+ qp . onDidHide ( ( ) => {
241
+ resolve ( undefined ) ;
242
+ } ) ,
243
+ qp . onDidTriggerItemButton ( async ( e ) => {
244
+ // When the User selects the secondary button, find or
245
+ // create the task in the tasks.json file
246
+
247
+ // There's only one button, so let's assert that
248
+ assert ( e . item . buttons && e . item . buttons [ 0 ] ) ;
249
+ assert ( e . button == e . item . buttons [ 0 ] ) ;
250
+
251
+ const tasks : vscode . TaskDefinition [ ] =
252
+ vscode . workspace . getConfiguration ( 'tasks' ) . get ( 'tasks' ) ?? [ ] ;
253
+
254
+ // Check if the task is already defined in tasks.json
255
+ if ( ! tasks . find ( ( t ) => t ?. label == getConventionalTaskLabel ( e . item . task ) ) ) {
256
+ // If the task doesn't exist, create it
257
+
258
+ // Copy the definition and add a label
259
+ const def : CustomTaskDefinition = {
260
+ ...( e . item . task . definition as CustomTaskDefinition ) ,
261
+ label : getConventionalTaskLabel ( e . item . task ) ,
262
+ } ;
263
+ tasks . push ( def ) ;
264
+ await vscode . workspace . getConfiguration ( ) . update ( 'tasks.tasks' , tasks ) ;
265
+ }
266
+
267
+ // Then open tasks.json in an editor
268
+ if ( vscode . workspace . workspaceFolders ) {
269
+ const tasksUri = vscode . workspace . workspaceFolders
270
+ . map ( ( ws ) => vscode . Uri . joinPath ( ws . uri , '.vscode' , 'tasks.json' ) )
271
+ . find ( ( v ) => existsSync ( v . fsPath ) ) ;
272
+ if ( tasksUri ) {
273
+ await vscode . window . showTextDocument ( tasksUri ) ;
274
+ }
275
+ }
276
+ resolve ( undefined ) ;
277
+ qp . hide ( ) ;
278
+ } )
279
+ ) ;
280
+
281
+ // Show the quick picker
282
+ qp . show ( ) ;
283
+ } ) ;
284
+
285
+ if ( choice ) {
286
+ // If a task was selected, mark it as the last executed task and
287
+ // run it
288
+ lastUsedTaskInfo = {
289
+ source : choice . task . source ,
290
+ name : choice . task . name ,
291
+ } ;
292
+ return await vscode . tasks . executeTask ( choice . task ) ;
293
+ } else {
294
+ return undefined ;
295
+ }
296
+ } finally {
297
+ disposables . forEach ( ( d ) => d . dispose ( ) ) ;
298
+ }
299
+ } else {
300
+ void vscode . window . showWarningMessage (
301
+ `There are no Mains defined in the workspace project ${ await getProjectFileRelPath ( ) } `
302
+ ) ;
303
+ return undefined ;
304
+ }
305
+ }
306
+
307
+ /**
308
+ *
309
+ * @returns Array of tasks of type `ada` and kind `buildAndRunMain`. This
310
+ * includes tasks automatically provided by the extension as well as
311
+ * user-defined tasks in tasks.json.
312
+ */
313
+ async function getBuildAndRunTasks ( ) {
314
+ return await vscode . tasks
315
+ . fetchTasks ( { type : 'ada' } )
316
+ . then ( ( tasks ) =>
317
+ tasks . filter (
318
+ ( t ) =>
319
+ ( t . definition as CustomTaskDefinition ) . configuration . kind == 'buildAndRunMain'
320
+ )
321
+ ) ;
322
+ }
0 commit comments