Skip to content

Commit

Permalink
Adds Create Camel project to the welcome screen
Browse files Browse the repository at this point in the history
- Rename NewCamelProjectCommand class to reflect its abstract nature.
- Create a 'generic' NewCamelProjectCommand that asks the user to select
  a runtime. Register the command with 'enablement = false' so that its
  does not show in the command palette.
- Refactor other classes with the new project structure.

Signed-off-by: Marcelo Henrique Diniz de Araujo <[email protected]>
  • Loading branch information
hdamarcelo committed Nov 30, 2024
1 parent 2139a58 commit b2341e3
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 154 deletions.
1 change: 0 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

## 1.6.0
- Provide folder selection when using `Create Camel Quarkus/SpringBoot Project` command

- Provide contextual menu to transform routes. In the `New Camel file` menu transform the right clicked files or all the file in a clicked folder.
- Update default Camel version used for Camel JBang from 4.8.0 to 4.8.1
- Update default Camel Catalog version from 4.8.0 to 4.8.1
Expand Down
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@
"category": "Camel",
"enablement": "workspaceFolderCount != 0"
},
{
"command": "camel.jbang.project.new",
"title": "Create a Camel project",
"category": "Camel",
"enablement": "false"
},
{
"command": "camel.jbang.project.quarkus.new",
"title": "Create a Camel Quarkus project",
Expand Down Expand Up @@ -328,6 +334,12 @@
"id": "camel.new.file",
"label": "New Camel File"
}
],
"viewsWelcome": [
{
"view": "workbench.explorer.emptyView",
"contents": "Create a new Camel project using the button below.\n[Create a Camel project](command:camel.jbang.project.new)\nLearn more about Language Support for Apache Camel by Red Hat by reading the [documentation](https://camel-tooling.github.io/camel-lsp-client-vscode/)"
}
]
},
"scripts": {
Expand Down
167 changes: 167 additions & 0 deletions src/commands/AbstractNewCamelProjectCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License", destination); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';

import * as path from "path";
import { TaskScope, Uri, commands, window, workspace } from "vscode";
import { CamelExportJBangTask } from "../tasks/CamelExportJBangTask";

export abstract class AbstractNewCamelProjectCommand {

async create() {
const runtime = await this.getRuntime();
const input = await this.askForGAV();
if (runtime && input) {

// Uses the user selected folder otherwise default to the root workspace folder
const outputFolder = await this.showDialogToPickFolder();
if (!outputFolder) {
await window.showErrorMessage('Operation canceled or invalid folder selection');
return;
}

const userChoice = await this.confirmDestructiveActionInSelectedFolder();

if (userChoice === undefined){
await window.showErrorMessage('Operation canceled');
return;
}

await new CamelExportJBangTask(TaskScope.Workspace, input, runtime, outputFolder.fsPath).execute();

// if not exist, init .vscode with tasks.json and launch.json config files
await workspace.fs.createDirectory(Uri.file(path.join(outputFolder.fsPath, '.vscode')));
for (const filename of ['tasks', 'launch']) {
await this.copyFile(`../../../resources/${runtime}/${filename}.json`, path.join(outputFolder.fsPath, `.vscode/${filename}.json`));
}

// open the newly created project in a new vscode instance
await commands.executeCommand('vscode.openFolder', outputFolder, true);

}
}

abstract getRuntime(): Promise<string|undefined>;

async askForGAV() {
return await window.showInputBox({
prompt: 'Please provide repository coordinate',
placeHolder: 'com.acme:myproject:1.0-SNAPSHOT',
value: 'com.acme:myproject:1.0-SNAPSHOT',
validateInput: (gav) => {
return this.validateGAV(gav);
},
});
}

/**
* Maven GAV validation
* - no empty name
* - Have 2 double-dots (similar check than Camel JBang)
* - following mostly recommendations from Maven Central for name rules
*
* @param name
* @returns string | undefined
*/
public validateGAV(name: string): string | undefined {
if (!name) {
return 'Please provide a GAV for the new project following groupId:artifactId:version pattern.';
}
if (name.includes(' ')) {
return 'The GAV cannot contain a space. It must constituted from groupId, artifactId and version following groupId:artifactId:version pattern.';
}
const gavs = name.split(':');
if (gavs.length !== 3) {
return 'The GAV needs to have double-dot `:` separator and constituted from groupId, artifactId and version';
}
const groupIdSplit = gavs[0].split('.');
if (groupIdSplit[0].length === 0) {
return 'The group id cannot start with a .';
}
for (const groupidSubPart of groupIdSplit) {
const regExpSearch = /^[a-z]\w*$/.exec(groupidSubPart);
if (regExpSearch === null || regExpSearch.length === 0) {
return `Invalid subpart of group Id: ${groupidSubPart}} . It must follow groupId:artifactId:version pattern with group Id subpart separated by dot needs to follow this specific pattern: [a-zA-Z]\\w*`;
}
}

const artifactId = gavs[1];
const regExpSearchArtifactId = /^[a-zA-Z]\w*$/.exec(artifactId);
if (regExpSearchArtifactId === null || regExpSearchArtifactId.length === 0) {
return `Invalid artifact Id: ${artifactId}} . It must follow groupId:artifactId:version pattern with artifactId specific pattern: [a-zA-Z]\\w*`;
}

const version = gavs[2];
const regExpSearch = /^\d[\w-.]*$/.exec(version);
if (regExpSearch === null || regExpSearch.length === 0) {
return `Invalid version: ${version} . It must follow groupId:artifactId:version pattern with version specific pattern: \\d[\\w-.]*`;
}
return undefined;
}

/**
* Open a dialog to select a folder to create the project in.
*
* @returns Uri of the selected folder or undefined if canceled by the user.
*/
private async showDialogToPickFolder(): Promise<Uri | undefined> {
const selectedFolders = await window.showOpenDialog(
{
canSelectMany: false,
canSelectFolders: true,
canSelectFiles: false,
openLabel: 'Select',
title: 'Select a folder to create the project in. ESC to cancel the project creation'
});
if (selectedFolders !== undefined) {
return selectedFolders[0];
}
return undefined;
}

/**
* Handles copy of the resources from the extension to the vscode workspace
*
* @param sourcePath Path of source
* @param destPath Path of destination
*/
private async copyFile(sourcePath: string, destPath: string): Promise<void> {

const sourcePathUri = Uri.file(path.resolve(__dirname, sourcePath));
const destPathUri = Uri.file(path.resolve(__dirname, destPath));
try {
await workspace.fs.copy(sourcePathUri, destPathUri, { overwrite: false });
} catch (error) {
// Do nothing in case there already exists tasks.json and launch.json files
}

}

/**
* Shows a modal asking for user confirmation of a potential desctructive action in the selected folder.
* VSCode automatically provides a 'Cancel' option which return `undefined`. The continue option will return the string `Continue`.
*
* @returns string | undefined
*/
private async confirmDestructiveActionInSelectedFolder() {
const message = 'Files in the selected folder WILL BE DELETED before project creation, continue?';
const continueOption = 'Continue';

return await window.showWarningMessage(message, { modal: true }, continueOption);

}
}
152 changes: 9 additions & 143 deletions src/commands/NewCamelProjectCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,152 +16,18 @@
*/
'use strict';

import * as path from "path";
import { TaskScope, Uri, commands, window, workspace } from "vscode";
import { CamelExportJBangTask } from "../tasks/CamelExportJBangTask";
import { window } from "vscode";
import { AbstractNewCamelProjectCommand } from "./AbstractNewCamelProjectCommand";
export class NewCamelProjectCommand extends AbstractNewCamelProjectCommand {

export abstract class NewCamelProjectCommand {
public static readonly ID_COMMAND_CAMEL_PROJECT = 'camel.jbang.project.new';

async create() {
const input = await this.askForGAV();
if (input) {

// Uses the user selected folder otherwise default to the root workspace folder
const outputFolder = await this.showDialogToPickFolder();
if (!outputFolder) {
await window.showErrorMessage('Operation canceled or invalid folder selection');
return;
}

const userChoice = await this.confirmDestructiveActionInSelectedFolder();

if (userChoice === undefined){
await window.showErrorMessage('Operation canceled');
return;
}

const runtime = this.getRuntime();
await new CamelExportJBangTask(TaskScope.Workspace, input, runtime, outputFolder.fsPath).execute();

// if not exist, init .vscode with tasks.json and launch.json config files
await workspace.fs.createDirectory(Uri.file(path.join(outputFolder.fsPath, '.vscode')));
for (const filename of ['tasks', 'launch']) {
await this.copyFile(`../../../resources/${runtime}/${filename}.json`, path.join(outputFolder.fsPath, `.vscode/${filename}.json`));
}

// open the newly created project in a new vscode instance
await commands.executeCommand('vscode.openFolder', outputFolder, true);

}
}

abstract getRuntime(): string;

async askForGAV() {
return await window.showInputBox({
prompt: 'Please provide repository coordinate',
placeHolder: 'com.acme:myproject:1.0-SNAPSHOT',
value: 'com.acme:myproject:1.0-SNAPSHOT',
validateInput: (gav) => {
return this.validateGAV(gav);
},
async getRuntime(): Promise<string|undefined> {
const options = ['quarkus', 'spring-boot'];
const selected = await window.showQuickPick(options, {
placeHolder: 'Select a runtime option',
});
return selected ?? undefined;
}

/**
* Maven GAV validation
* - no empty name
* - Have 2 double-dots (similar check than Camel JBang)
* - following mostly recommendations from Maven Central for name rules
*
* @param name
* @returns string | undefined
*/
public validateGAV(name: string): string | undefined {
if (!name) {
return 'Please provide a GAV for the new project following groupId:artifactId:version pattern.';
}
if (name.includes(' ')) {
return 'The GAV cannot contain a space. It must constituted from groupId, artifactId and version following groupId:artifactId:version pattern.';
}
const gavs = name.split(':');
if (gavs.length !== 3) {
return 'The GAV needs to have double-dot `:` separator and constituted from groupId, artifactId and version';
}
const groupIdSplit = gavs[0].split('.');
if (groupIdSplit[0].length === 0) {
return 'The group id cannot start with a .';
}
for (const groupidSubPart of groupIdSplit) {
const regExpSearch = /^[a-z]\w*$/.exec(groupidSubPart);
if (regExpSearch === null || regExpSearch.length === 0) {
return `Invalid subpart of group Id: ${groupidSubPart}} . It must follow groupId:artifactId:version pattern with group Id subpart separated by dot needs to follow this specific pattern: [a-zA-Z]\\w*`;
}
}

const artifactId = gavs[1];
const regExpSearchArtifactId = /^[a-zA-Z]\w*$/.exec(artifactId);
if (regExpSearchArtifactId === null || regExpSearchArtifactId.length === 0) {
return `Invalid artifact Id: ${artifactId}} . It must follow groupId:artifactId:version pattern with artifactId specific pattern: [a-zA-Z]\\w*`;
}

const version = gavs[2];
const regExpSearch = /^\d[\w-.]*$/.exec(version);
if (regExpSearch === null || regExpSearch.length === 0) {
return `Invalid version: ${version} . It must follow groupId:artifactId:version pattern with version specific pattern: \\d[\\w-.]*`;
}
return undefined;
}

/**
* Open a dialog to select a folder to create the project in.
*
* @returns Uri of the selected folder or undefined if canceled by the user.
*/
private async showDialogToPickFolder(): Promise<Uri | undefined> {
const selectedFolders = await window.showOpenDialog(
{
canSelectMany: false,
canSelectFolders: true,
canSelectFiles: false,
openLabel: 'Select',
title: 'Select a folder to create the project in. ESC to cancel the project creation'
});
if (selectedFolders !== undefined) {
return selectedFolders[0];
}
return undefined;
}

/**
* Handles copy of the resources from the extension to the vscode workspace
*
* @param sourcePath Path of source
* @param destPath Path of destination
*/
private async copyFile(sourcePath: string, destPath: string): Promise<void> {

const sourcePathUri = Uri.file(path.resolve(__dirname, sourcePath));
const destPathUri = Uri.file(path.resolve(__dirname, destPath));
try {
await workspace.fs.copy(sourcePathUri, destPathUri, { overwrite: false });
} catch (error) {
// Do nothing in case there already exists tasks.json and launch.json files
}

}

/**
* Shows a modal asking for user confirmation of a potential desctructive action in the selected folder.
* VSCode automatically provides a 'Cancel' option which return `undefined`. The continue option will return the string `Continue`.
*
* @returns string | undefined
*/
private async confirmDestructiveActionInSelectedFolder() {
const message = 'Files in the selected folder WILL BE DELETED before project creation, continue?';
const continueOption = 'Continue';

return await window.showWarningMessage(message, { modal: true }, continueOption);

}
}
8 changes: 4 additions & 4 deletions src/commands/NewCamelQuarkusProjectCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
*/
'use strict';

import { NewCamelProjectCommand } from "./NewCamelProjectCommand";
import { AbstractNewCamelProjectCommand } from "./AbstractNewCamelProjectCommand";

export class NewCamelQuarkusProjectCommand extends NewCamelProjectCommand {
export class NewCamelQuarkusProjectCommand extends AbstractNewCamelProjectCommand {

public static readonly ID_COMMAND_CAMEL_QUARKUS_PROJECT = 'camel.jbang.project.quarkus.new';

getRuntime(): string {
return 'quarkus';
getRuntime(): Promise<string> {
return Promise.resolve('quarkus');
}
}
8 changes: 4 additions & 4 deletions src/commands/NewCamelSpringBootProjectCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
*/
'use strict';

import { NewCamelProjectCommand } from "./NewCamelProjectCommand";
import { AbstractNewCamelProjectCommand } from "./AbstractNewCamelProjectCommand";

export class NewCamelSpringBootProjectCommand extends NewCamelProjectCommand {
export class NewCamelSpringBootProjectCommand extends AbstractNewCamelProjectCommand {

public static readonly ID_COMMAND_CAMEL_SPRINGBOOT_PROJECT = 'camel.jbang.project.springboot.new';

getRuntime(): string {
return 'spring-boot';
getRuntime(): Promise<string> {
return Promise.resolve('spring-boot');
}
}
Loading

0 comments on commit b2341e3

Please sign in to comment.