Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions extension.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ export { contextManager } from "./src/contextManager";
export { DependencyExplorer } from "./src/views/dependencyExplorer";
export { Commands } from "./src/commands";
export { LanguageServerMode } from "./src/languageServerApi/LanguageServerMode";

// tasks
export { BuildTaskProvider, categorizePaths, getFinalPaths } from "./src/tasks/build/buildTaskProvider";
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,40 @@
"description": "%taskDefinitions.java.project.exportJar.elements%"
}
}
},
{
"type": "java (build)",
"properties": {
"paths": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"enum": [
"${workspace}",
"!<path>"
],
"enumDescriptions": [
"%taskDefinitions.java.project.build.path.workspace%",
"%taskDefinitions.java.project.build.path.exclude%"
]
}
]
},
"default": [
"${workspace}"
],
"description": "%taskDefinitions.java.project.build.path%"
},
"isFullBuild": {
"type": "boolean",
"default": "true",
"description": "%taskDefinitions.java.project.build.isFullBuild%"
}
}
}
]
},
Expand Down
4 changes: 4 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"taskDefinitions.java.project.exportJar.testCompileOutput": "The folders containing output class files in the test scope.",
"taskDefinitions.java.project.exportJar.dependencies": "The artifact dependencies in the runtime scope.",
"taskDefinitions.java.project.exportJar.testDependencies": "The artifact dependencies in the test scope.",
"taskDefinitions.java.project.build.path": "The project root paths that will be built. Both absolute path and relative path to the workspace folder are supported.",
"taskDefinitions.java.project.build.path.workspace": "All the projects in workspace.",
"taskDefinitions.java.project.build.path.exclude": "The path after '!' will be excluded from the paths to be built.",
"taskDefinitions.java.project.build.isFullBuild": "Whether to execute a clean build or not.",
"viewsWelcome.workbench.createNewJavaProject": "You can also [open a Java project folder](command:_java.project.open), or create a new Java project by clicking the button below.\n[Create Java Project](command:java.project.create)",
"viewsWelcome.workbench.noJavaProject": "No Java projects found in the current workspace. You can [open a Java project folder](command:_java.project.open), or create a new Java project by clicking the button below.\n[Create Java Project](command:java.project.create)",
"viewsWelcome.workbench.inLightWeightMode": "To view the projects, you can import the projects into workspace.\n[Import Projects](command:java.server.mode.switch?%5B%22Standard%22,true%5D)",
Expand Down
4 changes: 4 additions & 0 deletions package.nls.zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"taskDefinitions.java.project.exportJar.testCompileOutput": "在 test scope 内包含输出的 class 文件的文件夹。",
"taskDefinitions.java.project.exportJar.dependencies": "在 runtime scope 内的依赖。",
"taskDefinitions.java.project.exportJar.testDependencies": "在 test scope 内的依赖。",
"taskDefinitions.java.project.build.path": "被构建项目的根目录路径。绝对路径或者相对于工作空间目录的相对路径都可以使用。",
"taskDefinitions.java.project.build.path.workspace": "工作空间中的所有项目。",
"taskDefinitions.java.project.build.path.exclude": "'!' 后的路径将会从待构建项目路径中移除。",
"taskDefinitions.java.project.build.isFullBuild": "是否要重新构建项目。",
"viewsWelcome.workbench.createNewJavaProject": "您也可以[打开一个 Java 项目目录](command:_java.project.open),或点击下方按钮创建一个新的 Java 项目。\n[创建 Java 项目](command:java.project.create)",
"viewsWelcome.workbench.noJavaProject": "当前工作空间未发现 Java 项目,您可以[打开一个 Java 项目目录](command:_java.project.open),或点击下方按钮创建一个新的 Java 项目。\n[创建 Java 项目](command:java.project.create)",
"viewsWelcome.workbench.inLightWeightMode": "要浏览项目信息,你可以将项目导入到工作空间中。\n[导入项目](command:java.server.mode.switch?%5B%22Standard%22,true%5D)",
Expand Down
2 changes: 1 addition & 1 deletion src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async function handleBuildFailure(operationId: string, err: any): Promise<boolea
return false;
}

function checkErrorsReportedByJavaExtension(): boolean {
export function checkErrorsReportedByJavaExtension(): boolean {
const problems = languages.getDiagnostics() || [];
for (const problem of problems) {
const fileName = basename(problem[0].fsPath || "");
Expand Down
8 changes: 8 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,18 @@ export namespace Commands {

export const WORKBENCH_ACTION_FILES_OPENFILEFOLDER = "workbench.action.files.openFileFolder";

export const WORKBENCH_VIEW_PROBLEMS = "workbench.actions.view.problems";

/**
* Commands from JLS
*/
export const LIST_SOURCEPATHS = "java.project.listSourcePaths";

export const COMPILE_WORKSPACE = "java.workspace.compile";

export const GET_ALL_PROJECTS = "java.project.getAll";

export const BUILD_PROJECT = "java.project.build";
}

export function executeJavaLanguageServerCommand(...rest: any[]) {
Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { commands, Extension, ExtensionContext, extensions, tasks, Uri, workspace } from "vscode";
import { dispose as disposeTelemetryWrapper, initializeFromJsonFile, instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
import { Commands, contextManager } from "../extension.bundle";
import { BuildTaskProvider } from "./tasks/build/buildTaskProvider";
import { Context, ExtensionName } from "./constants";
import { LibraryController } from "./controllers/libraryController";
import { ProjectController } from "./controllers/projectController";
Expand Down Expand Up @@ -38,6 +39,7 @@ async function activateExtension(_operationId: string, context: ExtensionContext
context.subscriptions.push(contextManager);
context.subscriptions.push(syncHandler);
context.subscriptions.push(tasks.registerTaskProvider(ExportJarTaskProvider.exportJarType, new ExportJarTaskProvider()));
context.subscriptions.push(tasks.registerTaskProvider(BuildTaskProvider.type, new BuildTaskProvider()));
}

// this method is called when your extension is deactivated
Expand Down
4 changes: 4 additions & 0 deletions src/java/jdtls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export namespace Jdtls {
return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_LIST, params) || [];
}

export async function getProjectUris(): Promise<string[]> {
return await commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.GET_ALL_PROJECTS) || [];
}

export async function refreshLibraries(params: string): Promise<boolean | undefined> {
return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_REFRESH_LIB_SERVER, params);
}
Expand Down
268 changes: 268 additions & 0 deletions src/tasks/build/buildTaskProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import { CancellationTokenSource, commands, CustomExecution, Event, EventEmitter, Pseudoterminal, Task,
TaskDefinition, TaskGroup, TaskProvider, TaskRevealKind, TaskScope, Uri, workspace, WorkspaceFolder } from "vscode";
import { Jdtls } from "../../java/jdtls";
import * as path from "path";
import { checkErrorsReportedByJavaExtension } from "../../build";
import { Commands } from "../../commands";

/**
* A task provider to provide Java build task support.
*/
export class BuildTaskProvider implements TaskProvider {

public static readonly type = "java (build)";

// tslint:disable-next-line: no-invalid-template-strings
public static readonly workspace = "${workspace}";
public static readonly defaultTaskName = "Build Workspace";

async provideTasks(): Promise<Task[]> {
const folders: readonly WorkspaceFolder[] = workspace.workspaceFolders || [];
if (!folders.length) {
return [];
}
const defaultTaskDefinition = {
type: BuildTaskProvider.type,
paths: [ BuildTaskProvider.workspace ],
isFullBuild: true,
};
const defaultTask = new Task(
defaultTaskDefinition,
TaskScope.Workspace,
BuildTaskProvider.defaultTaskName,
BuildTaskProvider.type,
new CustomExecution(async (resolvedDefinition: IBuildTaskDefinition): Promise<Pseudoterminal> => {
return new BuildTaskTerminal(resolvedDefinition, TaskScope.Workspace);
}),
);
defaultTask.detail = "$(tools) Build all the Java projects in workspace.";
defaultTask.group = TaskGroup.Build;
defaultTask.presentationOptions = {
reveal: TaskRevealKind.Never,
clear: true,
};
return [defaultTask];
}

async resolveTask(task: Task): Promise<Task | undefined> {
const taskDefinition = task.definition as IBuildTaskDefinition;
if (!taskDefinition.paths?.length) {
taskDefinition.paths = [ BuildTaskProvider.workspace ];
} else {
taskDefinition.paths = taskDefinition.paths
.map(p => p.trim())
.filter(Boolean);
task.definition = taskDefinition;
}
task.execution = new CustomExecution(async (resolvedDefinition: IBuildTaskDefinition): Promise<Pseudoterminal> => {
return new BuildTaskTerminal(resolvedDefinition, task.scope ?? TaskScope.Workspace);
});
task.presentationOptions = {
reveal: TaskRevealKind.Never,
clear: true,
};
return task;
}
}

class BuildTaskTerminal implements Pseudoterminal {

private cancellationTokenSource: CancellationTokenSource;

constructor(private readonly definition: IBuildTaskDefinition,
private readonly scope: WorkspaceFolder | TaskScope.Global | TaskScope.Workspace) {
this.cancellationTokenSource = new CancellationTokenSource();
}

writeEmitter = new EventEmitter<string>();
closeEmitter = new EventEmitter<number>();

onDidWrite: Event<string> = this.writeEmitter.event;
onDidClose: Event<number> = this.closeEmitter.event;

async open(): Promise<void> {
// TODO: consider change to terminal name via changeNameEmitter.
// see: https://github.com/microsoft/vscode/issues/154146

if (this.definition.paths.length === 1 &&
this.definition.paths[0] === BuildTaskProvider.workspace) {
await this.buildWorkspace();
} else {
await this.buildProjects();
}
this.writeEmitter.fire('Task complete.\r\n');
this.closeEmitter.fire(0);
}

close(): void {
this.cancellationTokenSource.cancel();
this.cancellationTokenSource.dispose();
}

async buildWorkspace(): Promise<void> {
this.writeEmitter.fire("Building all the Java projects in workspace...\r\n\r\n");
try {
await commands.executeCommand(Commands.COMPILE_WORKSPACE, this.definition.isFullBuild, this.cancellationTokenSource.token);
} catch (e) {
if (checkErrorsReportedByJavaExtension()) {
commands.executeCommand(Commands.WORKBENCH_VIEW_PROBLEMS);
this.writeEmitter.fire("Errors found when building the workspace.\r\n\r\n");
} else {
this.writeEmitter.fire("Errors occur when building the workspace:\r\n");
this.writeEmitter.fire(`${e}\r\n\r\n`);
}
}
}

async buildProjects(): Promise<void> {
// tslint:disable-next-line: prefer-const
let [includedPaths, excludedPaths, invalidPaths] = categorizePaths(this.definition.paths, this.scope);
if (invalidPaths.length) {
this.printList("Following paths are invalid, please provide absolute paths instead:", invalidPaths);
return;
}

const projectUris: string[] = await Jdtls.getProjectUris();
const projectPaths: string[] = projectUris
.map(uri => Uri.parse(uri).fsPath)
.filter(p => path.basename(p) !== "jdt.ls-java-project");
[includedPaths, invalidPaths] = getFinalPaths(includedPaths, excludedPaths, projectPaths);

if (invalidPaths.length) {
this.printList("Following paths are skipped due to not matching any project root path:", invalidPaths);
}

if (includedPaths.length === 0 || this.cancellationTokenSource.token.isCancellationRequested) {
return;
}

this.printList("Building following projects:", includedPaths);
const uris: Uri[] = includedPaths.map(p => Uri.file(p));
try {
const res = await commands.executeCommand(Commands.BUILD_PROJECT, uris, this.definition.isFullBuild,
this.cancellationTokenSource.token);
if (res === Jdtls.CompileWorkspaceStatus.Witherror && checkErrorsReportedByJavaExtension()) {
commands.executeCommand(Commands.WORKBENCH_VIEW_PROBLEMS);
}
} catch (e) {
this.writeEmitter.fire(`Error occurs when building the workspace: ${e}\r\n`);
}
}

private printList(title: string, list: string[]) {
this.writeEmitter.fire(`${title}\r\n`);
for (const l of list) {
this.writeEmitter.fire(` ${l}\r\n`);
}
this.writeEmitter.fire("\r\n");
}
}

/**
* Categorize the paths into three categories, and return the categories in an array.
* @param paths paths in the task definition.
* @param scope scope of the task
* @returns {Array} [included paths, excluded paths, invalid paths].
*/
export function categorizePaths(paths: string[], scope: WorkspaceFolder | TaskScope.Global | TaskScope.Workspace): string[][] {
const includes = [];
const excludes = [];
const invalid = [];
for (const p of paths) {
let actualPath = p;
const isNegative: boolean = p.startsWith("!");
if (isNegative) {
actualPath = trimNegativeSign(actualPath);
}

if (actualPath === BuildTaskProvider.workspace || path.isAbsolute(actualPath)) {
if (isNegative) {
excludes.push(actualPath);
} else {
includes.push(actualPath);
}
continue;
}

// global tasks are not supported now.
if (scope === TaskScope.Global) {
invalid.push(p);
continue;
}

let folder: WorkspaceFolder | undefined;
if (scope === TaskScope.Workspace) {
// cannot recover the absolute path
if (!workspace.workspaceFolders || workspace.workspaceFolders.length > 1) {
invalid.push(p);
} else {
folder = workspace.workspaceFolders[0];
}
}

if (!folder) {
continue;
}

const resolvedPath = path.join(folder.uri.fsPath, actualPath);
if (isNegative) {
excludes.push(resolvedPath);
} else {
includes.push(resolvedPath);
}
}
return [includes, excludes, invalid];
}

function trimNegativeSign(negativePath: string) {
let idx = 0;
for (; idx < negativePath.length; idx++) {
if (negativePath.charAt(idx) !== "!") {
break;
}
}
return negativePath.substring(idx);
}

/**
* Get the final paths which will be passed to the build projects command.
* @param includes included paths.
* @param excludes excluded paths.
* @param projectPaths paths of all the projects.
* @returns {Array} [ final paths, invalid paths ].
*/
export function getFinalPaths(includes: string[], excludes: string[], projectPaths: string[]): string[][] {
if (includes.includes(BuildTaskProvider.workspace)) {
includes = projectPaths;
}

includes = includes.filter(p => {
return !excludes.some(excludePath => path.relative(excludePath, p) === "");
});

const result: string[] = [];
const invalid: string[] = [];
for (const p of includes) {
const valid = projectPaths.some(projectPath => path.relative(projectPath, p) === "");
if (valid) {
result.push(p);
} else {
invalid.push(p);
}
}
return [result, invalid];
}

interface IBuildTaskDefinition extends TaskDefinition {
/**
* The root paths of the projects to be built.
*/
paths: string[];
/**
* Whether this is a full build or not.
*/
isFullBuild: boolean;
}
Loading