Skip to content

Commit e6de5b3

Browse files
Contribute Run and Debug menus to Project Explorer (#878)
1 parent f416ebc commit e6de5b3

File tree

4 files changed

+248
-129
lines changed

4 files changed

+248
-129
lines changed

package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@
6868
"title": "Debug",
6969
"icon": "$(debug-alt-small)"
7070
},
71+
{
72+
"command": "java.debug.runFromProjectView",
73+
"title": "Run"
74+
},
75+
{
76+
"command": "java.debug.debugFromProjectView",
77+
"title": "Debug"
78+
},
7179
{
7280
"command": "java.debug.continueAll",
7381
"title": "Continue All"
@@ -86,6 +94,18 @@
8694
}
8795
],
8896
"menus": {
97+
"view/item/context": [
98+
{
99+
"command": "java.debug.runFromProjectView",
100+
"when": "view == javaProjectExplorer && viewItem =~ /java:project(?=.*?\\b\\+java\\b)(?=.*?\\b\\+uri\\b)/",
101+
"group": "debugger@1"
102+
},
103+
{
104+
"command": "java.debug.debugFromProjectView",
105+
"when": "view == javaProjectExplorer && viewItem =~ /java:project(?=.*?\\b\\+java\\b)(?=.*?\\b\\+uri\\b)/",
106+
"group": "debugger@2"
107+
}
108+
],
89109
"explorer/context": [
90110
{
91111
"command": "java.debug.runJavaFile",
@@ -175,6 +195,14 @@
175195
{
176196
"command": "java.debug.pauseOthers",
177197
"when": "false"
198+
},
199+
{
200+
"command": "java.debug.runFromProjectView",
201+
"when": "false"
202+
},
203+
{
204+
"command": "java.debug.debugFromProjectView",
205+
"when": "false"
178206
}
179207
]
180208
},

src/configurationProvider.ts

Lines changed: 6 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as commands from "./commands";
1414
import * as lsPlugin from "./languageServerPlugin";
1515
import { addMoreHelpfulVMArgs, detectLaunchCommandStyle, validateRuntime } from "./launchCommand";
1616
import { logger, Type } from "./logger";
17+
import { mainClassPicker } from "./mainClassPicker";
1718
import { resolveJavaProcess } from "./processPicker";
1819
import * as utility from "./utility";
1920

@@ -26,7 +27,6 @@ const platformName = platformNameMappings[process.platform];
2627

2728
export class JavaDebugConfigurationProvider implements vscode.DebugConfigurationProvider {
2829
private isUserSettingsDirty: boolean = true;
29-
private debugHistory: MostRecentlyUsedHistory = new MostRecentlyUsedHistory();
3030
constructor() {
3131
vscode.workspace.onDidChangeConfiguration((event) => {
3232
if (event.affectsConfiguration("java.debug")) {
@@ -331,11 +331,8 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration
331331
const currentFile = config.mainClass || _.get(vscode.window.activeTextEditor, "document.uri.fsPath");
332332
if (currentFile) {
333333
const mainEntries = await lsPlugin.resolveMainMethod(vscode.Uri.file(currentFile));
334-
if (mainEntries.length === 1) {
335-
return mainEntries[0];
336-
} else if (mainEntries.length > 1) {
337-
return this.showMainClassQuickPick(this.formatMainClassOptions(mainEntries),
338-
`Please select a main class you want to run.`);
334+
if (mainEntries.length) {
335+
return mainClassPicker.showQuickPick(mainEntries, "Please select a main class you want to run.");
339336
}
340337
}
341338

@@ -384,9 +381,8 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration
384381
anchor: anchor.FAILED_TO_RESOLVE_CLASSPATH,
385382
}, "Fix");
386383
if (answer === "Fix") {
387-
const pickItems: IMainClassQuickPickItem[] = this.formatMainClassOptions(validationResponse.proposals);
388-
const selectedFix: lsPlugin.IMainClassOption =
389-
await this.showMainClassQuickPick(pickItems, "Please select main class<project name>.", false);
384+
const selectedFix = await mainClassPicker.showQuickPick(validationResponse.proposals,
385+
"Please select main class<project name>.", false);
390386
if (selectedFix) {
391387
sendInfo(null, {
392388
fix: "yes",
@@ -445,92 +441,7 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration
445441
});
446442
}
447443

448-
const pickItems: IMainClassQuickPickItem[] = this.formatRecentlyUsedMainClassOptions(res);
449-
const selected = await this.showMainClassQuickPick(pickItems, hintMessage || "Select main class<project name>");
450-
if (selected) {
451-
this.debugHistory.updateMRUTimestamp(selected);
452-
}
453-
454-
return selected;
455-
}
456-
457-
private async showMainClassQuickPick(pickItems: IMainClassQuickPickItem[], quickPickHintMessage: string, autoPick: boolean = true):
458-
Promise<lsPlugin.IMainClassOption | undefined> {
459-
// return undefined when the user cancels QuickPick by pressing ESC.
460-
const selected = (pickItems.length === 1 && autoPick) ?
461-
pickItems[0] : await vscode.window.showQuickPick(pickItems, { placeHolder: quickPickHintMessage });
462-
463-
return selected && selected.item;
464-
}
465-
466-
private formatRecentlyUsedMainClassOptions(options: lsPlugin.IMainClassOption[]): IMainClassQuickPickItem[] {
467-
// Sort the Main Class options with the recently used timestamp.
468-
options.sort((a: lsPlugin.IMainClassOption, b: lsPlugin.IMainClassOption) => {
469-
return this.debugHistory.getMRUTimestamp(b) - this.debugHistory.getMRUTimestamp(a);
470-
});
471-
472-
const mostRecentlyUsedOption: lsPlugin.IMainClassOption = (options.length && this.debugHistory.contains(options[0])) ? options[0] : undefined;
473-
const isMostRecentlyUsed = (option: lsPlugin.IMainClassOption): boolean => {
474-
return mostRecentlyUsedOption
475-
&& mostRecentlyUsedOption.mainClass === option.mainClass
476-
&& mostRecentlyUsedOption.projectName === option.projectName;
477-
};
478-
const isFromActiveEditor = (option: lsPlugin.IMainClassOption): boolean => {
479-
const activeEditor: vscode.TextEditor = vscode.window.activeTextEditor;
480-
const currentActiveFile: string = _.get(activeEditor, "document.uri.fsPath");
481-
return option.filePath && currentActiveFile && path.relative(option.filePath, currentActiveFile) === "";
482-
};
483-
const isPrivileged = (option: lsPlugin.IMainClassOption): boolean => {
484-
return isMostRecentlyUsed(option) || isFromActiveEditor(option);
485-
};
486-
487-
// Show the most recently used Main Class as the first one,
488-
// then the Main Class from Active Editor as second,
489-
// finally other Main Class.
490-
const adjustedOptions: lsPlugin.IMainClassOption[] = [];
491-
options.forEach((option: lsPlugin.IMainClassOption) => {
492-
if (isPrivileged(option)) {
493-
adjustedOptions.push(option);
494-
}
495-
});
496-
options.forEach((option: lsPlugin.IMainClassOption) => {
497-
if (!isPrivileged(option)) {
498-
adjustedOptions.push(option);
499-
}
500-
});
501-
502-
const pickItems: IMainClassQuickPickItem[] = this.formatMainClassOptions(adjustedOptions);
503-
pickItems.forEach((pickItem: IMainClassQuickPickItem) => {
504-
const adjustedDetail = [];
505-
if (isMostRecentlyUsed(pickItem.item)) {
506-
adjustedDetail.push("$(clock) recently used");
507-
}
508-
509-
if (isFromActiveEditor(pickItem.item)) {
510-
adjustedDetail.push(`$(file-text) active editor (${path.basename(pickItem.item.filePath)})`);
511-
}
512-
513-
pickItem.detail = adjustedDetail.join(", ");
514-
});
515-
516-
return pickItems;
517-
}
518-
519-
private formatMainClassOptions(options: lsPlugin.IMainClassOption[]): IMainClassQuickPickItem[] {
520-
return options.map((item) => {
521-
let label = item.mainClass;
522-
const description = item.filePath ? path.basename(item.filePath) : "";
523-
if (item.projectName) {
524-
label += `<${item.projectName}>`;
525-
}
526-
527-
return {
528-
label,
529-
description,
530-
detail: null,
531-
item,
532-
};
533-
});
444+
return mainClassPicker.showQuickPickWithRecentlyUsed(res, hintMessage || "Select main class<project name>");
534445
}
535446
}
536447

@@ -595,27 +506,3 @@ function convertLogLevel(commonLogLevel: string) {
595506
return "FINE";
596507
}
597508
}
598-
599-
export interface IMainClassQuickPickItem extends vscode.QuickPickItem {
600-
item: lsPlugin.IMainClassOption;
601-
}
602-
603-
class MostRecentlyUsedHistory {
604-
private cache: { [key: string]: number } = {};
605-
606-
public getMRUTimestamp(mainClassOption: lsPlugin.IMainClassOption): number {
607-
return this.cache[this.getKey(mainClassOption)] || 0;
608-
}
609-
610-
public updateMRUTimestamp(mainClassOption: lsPlugin.IMainClassOption): void {
611-
this.cache[this.getKey(mainClassOption)] = Date.now();
612-
}
613-
614-
public contains(mainClassOption: lsPlugin.IMainClassOption): boolean {
615-
return Boolean(this.cache[this.getKey(mainClassOption)]);
616-
}
617-
618-
private getKey(mainClassOption: lsPlugin.IMainClassOption): string {
619-
return mainClassOption.mainClass + "|" + mainClassOption.projectName;
620-
}
621-
}

src/extension.ts

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as _ from "lodash";
55
import * as path from "path";
66
import * as vscode from "vscode";
77
import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation,
8-
instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper";
8+
instrumentOperationAsVsCodeCommand, setUserError } from "vscode-extension-telemetry-wrapper";
99
import * as commands from "./commands";
1010
import { JavaDebugConfigurationProvider } from "./configurationProvider";
1111
import { HCR_EVENT, JAVA_LANGID, USER_NOTIFICATION_EVENT } from "./constants";
@@ -14,8 +14,9 @@ import { initializeCodeLensProvider, startDebugging } from "./debugCodeLensProvi
1414
import { handleHotCodeReplaceCustomEvent, initializeHotCodeReplace, NO_BUTTON, YES_BUTTON } from "./hotCodeReplace";
1515
import { JavaDebugAdapterDescriptorFactory } from "./javaDebugAdapterDescriptorFactory";
1616
import { logJavaException, logJavaInfo } from "./javaLogger";
17-
import { IMainMethod, resolveMainMethod } from "./languageServerPlugin";
17+
import { IMainClassOption, IMainMethod, resolveMainClass, resolveMainMethod } from "./languageServerPlugin";
1818
import { logger, Type } from "./logger";
19+
import { mainClassPicker } from "./mainClassPicker";
1920
import { pickJavaProcess } from "./processPicker";
2021
import { initializeThreadOperations } from "./threadOperations";
2122
import * as utility from "./utility";
@@ -60,6 +61,12 @@ function initializeExtension(operationId: string, context: vscode.ExtensionConte
6061
context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.debugJavaFile", async (uri: vscode.Uri) => {
6162
await runJavaFile(uri, false);
6263
}));
64+
context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.runFromProjectView", async (node: any) => {
65+
await runJavaProject(node, true);
66+
}));
67+
context.subscriptions.push(instrumentOperationAsVsCodeCommand("java.debug.debugFromProjectView", async (node: any) => {
68+
await runJavaProject(node, false);
69+
}));
6370
initializeHotCodeReplace(context);
6471
initializeCodeLensProvider(context);
6572
initializeThreadOperations(context);
@@ -261,17 +268,68 @@ async function runJavaFile(uri: vscode.Uri, noDebug: boolean) {
261268
return;
262269
}
263270

264-
const projectName = mainMethods[0].projectName;
265-
let mainClass = mainMethods[0].mainClass;
266-
if (mainMethods.length > 1) {
267-
mainClass = await vscode.window.showQuickPick(mainMethods.map((mainMethod) => mainMethod.mainClass), {
268-
placeHolder: "Select the main class to launch.",
269-
});
271+
const pick = await mainClassPicker.showQuickPick(mainMethods, "Select the main class to run.", (option) => option.mainClass);
272+
if (!pick) {
273+
return;
274+
}
275+
276+
await startDebugging(pick.mainClass, pick.projectName, uri, noDebug);
277+
}
278+
279+
async function runJavaProject(node: any, noDebug: boolean) {
280+
if (!node || !node.name || !node.uri) {
281+
vscode.window.showErrorMessage(`Failed to ${noDebug ? "run" : "debug"} the project because of invalid project node. `
282+
+ "This command only applies to Project Explorer view.");
283+
const error = new Error(`Failed to ${noDebug ? "run" : "debug"} the project because of invalid project node.`);
284+
setUserError(error);
285+
throw error;
286+
}
287+
288+
let mainClassesOptions: IMainClassOption[] = [];
289+
try {
290+
mainClassesOptions = await vscode.window.withProgress<IMainClassOption[]>(
291+
{
292+
location: vscode.ProgressLocation.Window,
293+
},
294+
async (p) => {
295+
p.report({
296+
message: "Searching main class...",
297+
});
298+
return resolveMainClass(vscode.Uri.parse(node.uri));
299+
});
300+
} catch (ex) {
301+
vscode.window.showErrorMessage(String((ex && ex.message) || ex));
302+
throw ex;
270303
}
271304

272-
if (!mainClass) {
305+
if (!mainClassesOptions || !mainClassesOptions.length) {
306+
vscode.window.showErrorMessage(`Failed to ${noDebug ? "run" : "debug"} this project '${node._nodeData.displayName || node.name}' `
307+
+ "because it does not contain any main class.");
273308
return;
274309
}
275310

276-
await startDebugging(mainClass, projectName, uri, noDebug);
311+
const pick = await mainClassPicker.showQuickPickWithRecentlyUsed(mainClassesOptions,
312+
"Select the main class to run.", (option) => option.mainClass);
313+
if (!pick) {
314+
return;
315+
}
316+
317+
const projectName: string = pick.projectName;
318+
const mainClass: string = pick.mainClass;
319+
const filePath: string = pick.filePath;
320+
const workspaceFolder: vscode.WorkspaceFolder = filePath ? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)) : undefined;
321+
const launchConfigurations: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("launch", workspaceFolder);
322+
const existingConfigs: vscode.DebugConfiguration[] = launchConfigurations.configurations;
323+
const existConfig: vscode.DebugConfiguration = _.find(existingConfigs, (config) => {
324+
return config.mainClass === mainClass && _.toString(config.projectName) === _.toString(projectName);
325+
});
326+
const debugConfig = existConfig || {
327+
type: "java",
328+
name: `Launch - ${mainClass.substr(mainClass.lastIndexOf(".") + 1)}`,
329+
request: "launch",
330+
mainClass,
331+
projectName,
332+
};
333+
debugConfig.noDebug = noDebug;
334+
vscode.debug.startDebugging(workspaceFolder, debugConfig);
277335
}

0 commit comments

Comments
 (0)