1+ import assert from 'assert' ;
2+ import { existsSync } from 'fs' ;
13import * as vscode from 'vscode' ;
24import { SymbolKind } from 'vscode' ;
5+ import { Disposable } from 'vscode-jsonrpc' ;
36import { ContextClients } from './clients' ;
47import { getOrAskForProgram } from './debugConfigProvider' ;
58import { mainOutputChannel } from './extension' ;
6- import { getEnclosingSymbol } from './taskProviders' ;
9+ import { getProjectFileRelPath } from './helpers' ;
10+ import { CustomTaskDefinition , getEnclosingSymbol } from './taskProviders' ;
711
812export function registerCommands ( context : vscode . ExtensionContext , clients : ContextClients ) {
913 context . subscriptions . push (
@@ -26,6 +30,14 @@ export function registerCommands(context: vscode.ExtensionContext, clients: Cont
2630 )
2731 ) ;
2832
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+
2941 // This is a hidden command that gets called in the default debug
3042 // configuration snippet that gets offered in the launch.json file.
3143 context . subscriptions . push (
@@ -89,3 +101,222 @@ async function addSupbrogramBoxCommand() {
89101 }
90102 } ) ;
91103}
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