microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
efb436fc3961824df9df06acf66d43fdf66ae087

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/commandPaletteHandler.ts

681lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import * as vscode from "vscode";
5import * as XDL from "./exponent/xdlInterface";
6import { SettingsHelper } from "./settingsHelper";
7import { OutputChannelLogger } from "./log/OutputChannelLogger";
8import { TargetType, GeneralMobilePlatform } from "./generalMobilePlatform";
9import { AndroidPlatform } from "./android/androidPlatform";
10import { IOSPlatform } from "./ios/iOSPlatform";
11import { ProjectVersionHelper } from "../common/projectVersionHelper";
12import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
13import { TargetPlatformHelper } from "../common/targetPlatformHelper";
14import { TelemetryHelper } from "../common/telemetryHelper";
15import { ProjectsStorage } from "./projectsStorage";
16import { IAndroidRunOptions, IIOSRunOptions, PlatformType } from "./launchArgs";
17import { ExponentPlatform } from "./exponent/exponentPlatform";
18import { spawn, ChildProcess } from "child_process";
19import { HostPlatform } from "../common/hostPlatform";
20import { LaunchJsonCompletionHelper } from "../common/launchJsonCompletionHelper";
21import { ReactNativeDebugConfigProvider } from "./debuggingConfiguration/reactNativeDebugConfigProvider";
22import { CommandExecutor } from "../common/commandExecutor";
23import * as nls from "vscode-nls";
24import { ErrorHelper } from "../common/error/errorHelper";
25import { InternalErrorCode } from "../common/error/internalErrorCode";
26import { AppLauncher } from "./appLauncher";
27import { AndroidEmulatorManager } from "./android/androidEmulatorManager";
28import { AdbHelper } from "./android/adb";
29import { LogCatMonitor } from "./android/logCatMonitor";
30import { LogCatMonitorManager } from "./android/logCatMonitorManager";
31nls.config({
32 messageFormat: nls.MessageFormat.bundle,
33 bundleFormat: nls.BundleFormat.standalone,
34})();
35const localize = nls.loadMessageBundle();
36
37export class CommandPaletteHandler {
38 public static elementInspector: ChildProcess | null;
39 private static logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
40
41 /**
42 * Starts the React Native packager
43 */
44 public static startPackager(): Promise<void> {
45 return this.selectProject().then((appLauncher: AppLauncher) => {
46 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
47 appLauncher.getPackager().getProjectPath(),
48 // eslint-disable-next-line @typescript-eslint/no-unused-vars
49 ).then(versions => {
50 return this.executeCommandInContext(
51 "startPackager",
52 appLauncher.getWorkspaceFolder(),
53 () => {
54 return appLauncher
55 .getPackager()
56 .isRunning()
57 .then(running => {
58 return running
59 ? appLauncher.getPackager().stop()
60 : Promise.resolve();
61 });
62 },
63 ).then(() => appLauncher.getPackager().start());
64 });
65 });
66 }
67
68 /**
69 * Kills the React Native packager invoked by the extension's packager
70 */
71 public static stopPackager(): Promise<void> {
72 return this.selectProject().then((appLauncher: AppLauncher) => {
73 return this.executeCommandInContext(
74 "stopPackager",
75 appLauncher.getWorkspaceFolder(),
76 () => appLauncher.getPackager().stop(),
77 );
78 });
79 }
80
81 public static stopAllPackagers(): Promise<void> {
82 let keys = Object.keys(ProjectsStorage.projectsCache);
83 let promises: Promise<void>[] = [];
84 keys.forEach(key => {
85 let appLauncher = ProjectsStorage.projectsCache[key];
86 promises.push(
87 this.executeCommandInContext("stopPackager", appLauncher.getWorkspaceFolder(), () =>
88 appLauncher.getPackager().stop(),
89 ),
90 );
91 });
92
93 return Promise.all(promises).then(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
94 }
95
96 /**
97 * Restarts the React Native packager
98 */
99 public static restartPackager(): Promise<void> {
100 return this.selectProject().then((appLauncher: AppLauncher) => {
101 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
102 appLauncher.getPackager().getProjectPath(),
103 // eslint-disable-next-line @typescript-eslint/no-unused-vars
104 ).then(versions => {
105 return this.executeCommandInContext(
106 "restartPackager",
107 appLauncher.getWorkspaceFolder(),
108 () => this.runRestartPackagerCommandAndUpdateStatus(appLauncher),
109 );
110 });
111 });
112 }
113
114 /**
115 * Execute command to publish to exponent host.
116 */
117 public static publishToExpHost(): Promise<void> {
118 return this.selectProject().then((appLauncher: AppLauncher) => {
119 return this.executeCommandInContext(
120 "publishToExpHost",
121 appLauncher.getWorkspaceFolder(),
122 () => {
123 return this.executePublishToExpHost(appLauncher).then(didPublish => {
124 if (!didPublish) {
125 CommandPaletteHandler.logger.warning(
126 localize(
127 "ExponentPublishingWasUnsuccessfulMakeSureYoureLoggedInToExpo",
128 "Publishing was unsuccessful. Please make sure you are logged in Expo and your project is a valid Expo project",
129 ),
130 );
131 }
132 });
133 },
134 );
135 });
136 }
137
138 public static async launchAndroidEmulator(): Promise<void> {
139 const appLauncher = await this.selectProject();
140 const adbHelper = new AdbHelper(appLauncher.getPackager().getProjectPath());
141 const androidEmulatorManager = new AndroidEmulatorManager(adbHelper);
142 const emulator = await androidEmulatorManager.startSelection();
143 if (emulator) {
144 androidEmulatorManager.tryLaunchEmulatorByName(emulator);
145 }
146 }
147
148 /**
149 * Executes the 'react-native run-android' command
150 */
151 public static runAndroid(target: TargetType = "simulator"): Promise<void> {
152 return this.selectProject().then((appLauncher: AppLauncher) => {
153 TargetPlatformHelper.checkTargetPlatformSupport(PlatformType.Android);
154 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
155 appLauncher.getPackager().getProjectPath(),
156 ).then(versions => {
157 appLauncher.setReactNativeVersions(versions);
158 return this.executeCommandInContext(
159 "runAndroid",
160 appLauncher.getWorkspaceFolder(),
161 () => {
162 const platform = <AndroidPlatform>(
163 this.createPlatform(
164 appLauncher,
165 PlatformType.Android,
166 AndroidPlatform,
167 target,
168 )
169 );
170 return platform
171 .resolveVirtualDevice(target)
172 .then(() => platform.beforeStartPackager())
173 .then(() => {
174 return platform.startPackager();
175 })
176 .then(() => {
177 return platform.runApp(/*shouldLaunchInAllDevices*/ true);
178 })
179 .then(() => {
180 return platform.disableJSDebuggingMode();
181 });
182 },
183 );
184 });
185 });
186 }
187
188 /**
189 * Executes the 'react-native run-ios' command
190 */
191 public static runIos(target: TargetType = "simulator"): Promise<void> {
192 return this.selectProject().then((appLauncher: AppLauncher) => {
193 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
194 appLauncher.getPackager().getProjectPath(),
195 ).then(versions => {
196 appLauncher.setReactNativeVersions(versions);
197 TargetPlatformHelper.checkTargetPlatformSupport(PlatformType.iOS);
198 return this.executeCommandInContext(
199 "runIos",
200 appLauncher.getWorkspaceFolder(),
201 () => {
202 const platform = <IOSPlatform>(
203 this.createPlatform(appLauncher, PlatformType.iOS, IOSPlatform, target)
204 );
205 return (
206 platform
207 .resolveVirtualDevice(target)
208 .then(() => platform.beforeStartPackager())
209 .then(() => {
210 return platform.startPackager();
211 })
212 .then(() => {
213 // Set the Debugging setting to disabled, because in iOS it's persisted across runs of the app
214 return platform.disableJSDebuggingMode();
215 })
216 // eslint-disable-next-line @typescript-eslint/no-empty-function
217 .catch(() => {}) // If setting the debugging mode fails, we ignore the error and we run the run ios command anyways
218 .then(() => {
219 return platform.runApp();
220 })
221 );
222 },
223 );
224 });
225 });
226 }
227
228 /**
229 * Starts the Exponent packager
230 */
231 public static runExponent(): Promise<void> {
232 return this.selectProject().then((appLauncher: AppLauncher) => {
233 return ProjectVersionHelper.getReactNativePackageVersionsFromNodeModules(
234 appLauncher.getPackager().getProjectPath(),
235 ).then(versions => {
236 return this.loginToExponent(appLauncher).then(() => {
237 return this.executeCommandInContext(
238 "runExponent",
239 appLauncher.getWorkspaceFolder(),
240 () => {
241 appLauncher.setReactNativeVersions(versions);
242 const platform = <ExponentPlatform>(
243 this.createPlatform(
244 appLauncher,
245 PlatformType.Exponent,
246 ExponentPlatform,
247 )
248 );
249 return platform
250 .beforeStartPackager()
251 .then(() => {
252 return platform.startPackager();
253 })
254 .then(() => {
255 return platform.runApp();
256 });
257 },
258 );
259 });
260 });
261 });
262 }
263
264 public static showDevMenu(): Promise<void> {
265 return this.selectProject().then((appLauncher: AppLauncher) => {
266 const androidPlatform = <AndroidPlatform>(
267 this.createPlatform(appLauncher, PlatformType.Android, AndroidPlatform)
268 );
269 androidPlatform
270 .showDevMenu()
271 // eslint-disable-next-line @typescript-eslint/no-empty-function
272 .catch(() => {}); // Ignore any errors
273
274 if (process.platform === "darwin") {
275 const iosPlatform = <IOSPlatform>(
276 this.createPlatform(appLauncher, PlatformType.iOS, IOSPlatform)
277 );
278 iosPlatform
279 .showDevMenu(appLauncher)
280 // eslint-disable-next-line @typescript-eslint/no-empty-function
281 .catch(() => {}); // Ignore any errors
282 }
283 return Promise.resolve();
284 });
285 }
286
287 public static reloadApp(): Promise<void> {
288 return this.selectProject().then((appLauncher: AppLauncher) => {
289 const androidPlatform = <AndroidPlatform>(
290 this.createPlatform(appLauncher, PlatformType.Android, AndroidPlatform)
291 );
292 androidPlatform
293 .reloadApp()
294 // eslint-disable-next-line @typescript-eslint/no-empty-function
295 .catch(() => {}); // Ignore any errors
296
297 if (process.platform === "darwin") {
298 const iosPlatform = <IOSPlatform>(
299 this.createPlatform(appLauncher, PlatformType.iOS, IOSPlatform)
300 );
301 iosPlatform
302 .reloadApp(appLauncher)
303 // eslint-disable-next-line @typescript-eslint/no-empty-function
304 .catch(() => {}); // Ignore any errors
305 }
306 return Promise.resolve();
307 });
308 }
309
310 public static runElementInspector(): Promise<void> {
311 if (!CommandPaletteHandler.elementInspector) {
312 // Remove the following env variables to prevent running electron app in node mode.
313 // https://github.com/microsoft/vscode/issues/3011#issuecomment-184577502
314 let env = Object.assign({}, process.env);
315 delete env.ATOM_SHELL_INTERNAL_RUN_AS_NODE;
316 delete env.ELECTRON_RUN_AS_NODE;
317 let command = HostPlatform.getNpmCliCommand("react-devtools");
318 CommandPaletteHandler.elementInspector = spawn(command, [], {
319 env,
320 });
321 if (!CommandPaletteHandler.elementInspector.pid) {
322 CommandPaletteHandler.elementInspector = null;
323 return Promise.reject(
324 ErrorHelper.getInternalError(InternalErrorCode.ReactDevtoolsIsNotInstalled),
325 );
326 }
327 CommandPaletteHandler.elementInspector.stdout.on("data", (data: string) => {
328 this.logger.info(data);
329 });
330 CommandPaletteHandler.elementInspector.stderr.on("data", (data: string) => {
331 this.logger.error(data);
332 });
333 CommandPaletteHandler.elementInspector.once("exit", () => {
334 CommandPaletteHandler.elementInspector = null;
335 });
336 } else {
337 this.logger.info(
338 localize(
339 "AnotherElementInspectorAlreadyRun",
340 "Another element inspector already run",
341 ),
342 );
343 }
344 return Promise.resolve();
345 }
346
347 public static stopElementInspector(): void {
348 return CommandPaletteHandler.elementInspector
349 ? CommandPaletteHandler.elementInspector.kill()
350 : void 0;
351 }
352
353 public static getPlatformByCommandName(commandName: string): string {
354 commandName = commandName.toLocaleLowerCase();
355
356 if (commandName.indexOf(PlatformType.Android) > -1) {
357 return PlatformType.Android;
358 }
359
360 if (commandName.indexOf(PlatformType.iOS) > -1) {
361 return PlatformType.iOS;
362 }
363
364 if (commandName.indexOf(PlatformType.Exponent) > -1) {
365 return PlatformType.Exponent;
366 }
367
368 return "";
369 }
370
371 public static startLogCatMonitor(): Promise<void> {
372 return this.selectProject().then(appLauncher => {
373 const adbHelper = new AdbHelper(appLauncher.getPackager().getProjectPath());
374 const avdManager = new AndroidEmulatorManager(adbHelper);
375 return avdManager.selectOnlineDevice().then(deviceId => {
376 if (deviceId) {
377 LogCatMonitorManager.delMonitor(deviceId); // Stop previous logcat monitor if it's running
378 let logCatArguments = SettingsHelper.getLogCatFilteringArgs(
379 appLauncher.getWorkspaceFolderUri(),
380 );
381 // this.logCatMonitor can be mutated, so we store it locally too
382 let logCatMonitor = new LogCatMonitor(deviceId, adbHelper, logCatArguments);
383 LogCatMonitorManager.addMonitor(logCatMonitor);
384 logCatMonitor
385 .start() // The LogCat will continue running forever, so we don't wait for it
386 .catch(() =>
387 this.logger.warning(
388 localize(
389 "ErrorWhileMonitoringLogCat",
390 "Error while monitoring LogCat",
391 ),
392 ),
393 );
394 } else {
395 vscode.window.showErrorMessage(
396 localize(
397 "OnlineAndroidDeviceNotFound",
398 "Could not find a proper online Android device to start a LogCat monitor",
399 ),
400 );
401 }
402 });
403 });
404 }
405
406 public static stopLogCatMonitor(): Promise<void> {
407 return this.selectLogCatMonitor().then(monitor => {
408 LogCatMonitorManager.delMonitor(monitor.deviceId);
409 });
410 }
411
412 public static async selectAndInsertDebugConfiguration(
413 configurationProvider: ReactNativeDebugConfigProvider,
414 document: vscode.TextDocument,
415 position: vscode.Position,
416 token: vscode.CancellationToken,
417 ): Promise<void> {
418 if (
419 vscode.window.activeTextEditor &&
420 vscode.window.activeTextEditor.document === document
421 ) {
422 const folder = vscode.workspace.getWorkspaceFolder(document.uri);
423 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
424 const config = await configurationProvider.provideDebugConfigurationSequentially!(
425 folder,
426 token,
427 );
428
429 if (!token.isCancellationRequested && config) {
430 // Always use the first available debug configuration.
431 const cursorPosition = LaunchJsonCompletionHelper.getCursorPositionInConfigurationsArray(
432 document,
433 position,
434 );
435 if (!cursorPosition) {
436 return;
437 }
438 const commaPosition = LaunchJsonCompletionHelper.isCommaImmediatelyBeforeCursor(
439 document,
440 position,
441 )
442 ? "BeforeCursor"
443 : undefined;
444 const formattedJson = LaunchJsonCompletionHelper.getTextForInsertion(
445 config,
446 cursorPosition,
447 commaPosition,
448 );
449 const workspaceEdit = new vscode.WorkspaceEdit();
450 workspaceEdit.insert(document.uri, position, formattedJson);
451 await vscode.workspace.applyEdit(workspaceEdit);
452 vscode.commands.executeCommand("editor.action.formatDocument").then(
453 () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
454 () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
455 );
456 }
457 }
458 }
459
460 private static createPlatform(
461 appLauncher: AppLauncher,
462 platform: PlatformType.iOS | PlatformType.Android | PlatformType.Exponent,
463 platformClass: typeof GeneralMobilePlatform,
464 target?: TargetType,
465 ): GeneralMobilePlatform {
466 const runOptions = CommandPaletteHandler.getRunOptions(appLauncher, platform, target);
467 return new platformClass(runOptions, {
468 packager: appLauncher.getPackager(),
469 });
470 }
471
472 private static runRestartPackagerCommandAndUpdateStatus(
473 appLauncher: AppLauncher,
474 ): Promise<void> {
475 return appLauncher
476 .getPackager()
477 .restart(SettingsHelper.getPackagerPort(appLauncher.getWorkspaceFolderUri().fsPath));
478 }
479
480 /**
481 * Ensures that we are in a React Native project and then executes the operation
482 * Otherwise, displays an error message banner
483 * {operation} - a function that performs the expected operation
484 */
485 private static executeCommandInContext(
486 rnCommand: string,
487 workspaceFolder: vscode.WorkspaceFolder,
488 operation: () => Promise<void>,
489 ): Promise<void> {
490 const extProps = {
491 platform: {
492 value: CommandPaletteHandler.getPlatformByCommandName(rnCommand),
493 isPii: false,
494 },
495 };
496
497 return TelemetryHelper.generate("RNCommand", extProps, generator => {
498 generator.add("command", rnCommand, false);
499 const projectRoot = SettingsHelper.getReactNativeProjectRoot(
500 workspaceFolder.uri.fsPath,
501 );
502 this.logger.debug(`Command palette: run project ${projectRoot} in context`);
503 return ReactNativeProjectHelper.isReactNativeProject(projectRoot).then(isRNProject => {
504 generator.add("isRNProject", isRNProject, false);
505 if (isRNProject) {
506 // Bring the log channel to focus
507 this.logger.setFocusOnLogChannel();
508
509 // Execute the operation
510 return operation();
511 } else {
512 vscode.window.showErrorMessage(
513 `${projectRoot} workspace is not a React Native project.`,
514 );
515 return;
516 }
517 });
518 });
519 }
520
521 /**
522 * Publish project to exponent server. In order to do this we need to make sure the user is logged in exponent and the packager is running.
523 */
524 private static executePublishToExpHost(appLauncher: AppLauncher): Promise<boolean> {
525 CommandPaletteHandler.logger.info(
526 localize(
527 "PublishingAppToExponentServer",
528 "Publishing app to Expo server. This might take a moment.",
529 ),
530 );
531 return this.loginToExponent(appLauncher).then(user => {
532 CommandPaletteHandler.logger.debug(`Publishing as ${user.username}...`);
533 return this.runExponent()
534 .then(() => XDL.publish(appLauncher.getWorkspaceFolderUri().fsPath))
535 .then(response => {
536 if (response.err || !response.url) {
537 return false;
538 }
539 const publishedOutput = localize(
540 "ExpoAppSuccessfullyPublishedTo",
541 "Expo app successfully published to {0}",
542 response.url,
543 );
544 CommandPaletteHandler.logger.info(publishedOutput);
545 vscode.window.showInformationMessage(publishedOutput);
546 return true;
547 });
548 });
549 }
550
551 private static loginToExponent(appLauncher: AppLauncher): Promise<XDL.IUser> {
552 return appLauncher
553 .getExponentHelper()
554 .loginToExponent(
555 (message, password) => {
556 return new Promise((resolve, reject) => {
557 vscode.window
558 .showInputBox({ placeHolder: message, password: password })
559 .then(login => {
560 resolve(login || "");
561 }, reject);
562 });
563 },
564 message => {
565 return new Promise((resolve, reject) => {
566 vscode.window.showInformationMessage(message).then(password => {
567 resolve(password || "");
568 }, reject);
569 });
570 },
571 )
572 .catch(err => {
573 CommandPaletteHandler.logger.warning(
574 localize(
575 "ExpoErrorOccuredMakeSureYouAreLoggedIn",
576 "An error has occured. Please make sure you are logged in to Expo, your project is setup correctly for publishing and your packager is running as Expo.",
577 ),
578 );
579 throw err;
580 });
581 }
582
583 private static selectProject(): Promise<AppLauncher> {
584 let keys = Object.keys(ProjectsStorage.projectsCache);
585 if (keys.length > 1) {
586 return new Promise((resolve, reject) => {
587 vscode.window.showQuickPick(keys).then(selected => {
588 if (selected) {
589 this.logger.debug(`Command palette: selected project ${selected}`);
590 resolve(ProjectsStorage.projectsCache[selected]);
591 }
592 }, reject);
593 });
594 } else if (keys.length === 1) {
595 this.logger.debug(`Command palette: once project ${keys[0]}`);
596 return Promise.resolve(ProjectsStorage.projectsCache[keys[0]]);
597 } else {
598 return Promise.reject(
599 ErrorHelper.getInternalError(
600 InternalErrorCode.WorkspaceNotFound,
601 "Current workspace does not contain React Native projects.",
602 ),
603 );
604 }
605 }
606
607 private static selectLogCatMonitor(): Promise<LogCatMonitor> {
608 let keys = Object.keys(LogCatMonitorManager.logCatMonitorsCache);
609 if (keys.length > 1) {
610 return new Promise((resolve, reject) => {
611 vscode.window.showQuickPick(keys).then(selected => {
612 if (selected) {
613 this.logger.debug(`Command palette: selected LogCat monitor ${selected}`);
614 resolve(LogCatMonitorManager.logCatMonitorsCache[selected]);
615 }
616 }, reject);
617 });
618 } else if (keys.length === 1) {
619 this.logger.debug(`Command palette: once LogCat monitor ${keys[0]}`);
620 return Promise.resolve(LogCatMonitorManager.logCatMonitorsCache[keys[0]]);
621 } else {
622 return Promise.reject(
623 ErrorHelper.getInternalError(
624 InternalErrorCode.AndroidCouldNotFindActiveLogCatMonitor,
625 ),
626 );
627 }
628 }
629
630 private static getRunOptions(
631 appLauncher: AppLauncher,
632 platform: PlatformType.iOS | PlatformType.Android | PlatformType.Exponent,
633 target: TargetType = "simulator",
634 ): IAndroidRunOptions | IIOSRunOptions {
635 const packagerPort = SettingsHelper.getPackagerPort(
636 appLauncher.getWorkspaceFolderUri().fsPath,
637 );
638 const runArgs = SettingsHelper.getRunArgs(
639 platform,
640 target,
641 appLauncher.getWorkspaceFolderUri(),
642 );
643 const envArgs = SettingsHelper.getEnvArgs(
644 platform,
645 target,
646 appLauncher.getWorkspaceFolderUri(),
647 );
648 const envFile = SettingsHelper.getEnvFile(
649 platform,
650 target,
651 appLauncher.getWorkspaceFolderUri(),
652 );
653 const projectRoot = SettingsHelper.getReactNativeProjectRoot(
654 appLauncher.getWorkspaceFolderUri().fsPath,
655 );
656 const runOptions: IAndroidRunOptions | IIOSRunOptions = {
657 platform: platform,
658 workspaceRoot: appLauncher.getWorkspaceFolderUri().fsPath,
659 projectRoot: projectRoot,
660 packagerPort: packagerPort,
661 runArguments: runArgs,
662 env: envArgs,
663 envFile: envFile,
664 reactNativeVersions: appLauncher.getReactNativeVersions() || {
665 reactNativeVersion: "",
666 reactNativeWindowsVersion: "",
667 reactNativeMacOSVersion: "",
668 },
669 };
670
671 if (platform === PlatformType.iOS && target === "device") {
672 runOptions.target = "device";
673 }
674
675 CommandExecutor.ReactNativeCommand = SettingsHelper.getReactNativeGlobalCommandName(
676 appLauncher.getWorkspaceFolderUri(),
677 );
678
679 return runOptions;
680 }
681}
682