Skip to content

Commit

Permalink
feat: Add custom template in 'New Script' wizard
Browse files Browse the repository at this point in the history
Signed-off-by: Fred Bricon <[email protected]>
  • Loading branch information
fbricon committed Aug 30, 2022
1 parent 35a9507 commit 644d5b1
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 26 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This is an early work-in-progress extension for [vscode-java](https://marketplace.visualstudio.com/items?itemName=redhat.java). It aims at providing support for the [JBang](https://www.jbang.dev/) scripts written in Java.

**Pre-requisites:**
- [JBang](https://www.jbang.dev/download/) is installed and available in the PATH.
- [JBang](https://www.jbang.dev/download/) is installed and available in the PATH. Alternatively, you can set the `jbang.home` preference to point to a `JBang` installation
- [vscode-java](https://marketplace.visualstudio.com/items?itemName=redhat.java) is installed.

**Outstanding issues**:
Expand All @@ -18,6 +18,10 @@ This is an early work-in-progress extension for [vscode-java](https://marketplac
- Create a new JBang script from an existing template with the `JBang: Create a new script` command.
- Annotation processors are automatically detected and configured.

## Preferences
- `jbang.home`: Specifies the folder path to the JBang directory (not the executable), eg. `~/.sdkman/candidates/jbang/current`. On Windows, backslashes must be escaped, eg `C:\\ProgramData\\chocolatey\\lib\\jbang`. Used by the `JBang: Create a new script` wizard and the `Run JBang` code lens. Useful in case `jbang` is not automatically picked up from the $PATH, for some reason.
- `jbang.wizard.templates.showDescriptions` : When set to `true` (the default), shows JBang template descriptions in the `JBang: Create a new script` wizard, else hides them.

## Installation:
Continuous Integration builds can be installed from [https://github.com/jbangdev/jbang-vscode/releases/tag/latest](https://github.com/jbangdev/jbang-vscode/releases/tag/latest). Download the most recent `jbang-vscode-<version>.vsix` file and install it by following the instructions [here](https://code.visualstudio.com/docs/editor/extension-gallery#_install-from-a-vsix).

Expand Down
1 change: 1 addition & 0 deletions assets/buttons/info_dark_theme.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/buttons/info_light_theme.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@
"group": "1_javaactions"
}
]
},
"configuration": {
"title": "JBang",
"properties": {
"jbang.home": {
"type": [
"string",
"null"
],
"default": null,
"markdownDescription": "Specifies the folder path to the JBang directory (not the executable), eg. `~/.sdkman/candidates/jbang/current`.\n\nOn Windows, backslashes must be escaped, eg. `C:\\\\ProgramData\\\\chocolatey\\\\lib\\\\jbang`.\n\nUsed by the `JBang: Create a new script` wizard and the `Run JBang` code lens. Useful in case `jbang` is not automatically picked up from the $PATH, for some reason.",
"description": "Specifies the folder path to the JBang directory (not the executable), eg. '~/.sdkman/candidates/jbang/current'.\n\nOn Windows, backslashes must be escaped, eg. 'C:\\\\ProgramData\\\\chocolatey\\\\lib\\\\jbang'.\n\nUsed by the 'JBang: Create a new script' wizard and the 'Run JBang' code lens. Useful in case 'jbang' is not automatically picked up from the $PATH, for some reason.",
"scope": "machine-overridable"
},
"jbang.wizard.templates.showDescriptions": {
"type": "boolean",
"default": true,
"markdownDescription": "When set to `true` (the default), shows JBang template descriptions in the `JBang: Create a new script` wizard, else hides them.",
"description": "When set to 'true' (the default), shows JBang template descriptions in the 'JBang: Create a new script' wizard, else hides them.",
"scope": "window"
}
}
}
},
"scripts": {
Expand Down
15 changes: 15 additions & 0 deletions src/Assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import path = require("path");
import { ExtensionContext, Uri } from "vscode";

export namespace Assets {

let extensionPath: string;

export function initialize(extensionContext: ExtensionContext) {
extensionPath = extensionContext.extensionPath;
}

export function get(assetName: string): Uri {
return Uri.file(path.join(extensionPath, 'assets', assetName));
}
}
35 changes: 35 additions & 0 deletions src/JBangConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ConfigurationTarget, workspace, WorkspaceConfiguration } from "vscode";

export namespace JBangConfig {

export const SHOW_TEMPLATE_DESC = 'wizard.templates.showDescriptions';
export const HOME = 'home';

export function isShowTemplateDescriptions(): boolean {
return getJBangConfiguration().get<boolean>(SHOW_TEMPLATE_DESC, true);
}

export function getJBangHome(): string|undefined {
return (getJBangConfiguration().get(HOME) as string|undefined)?.trim();
}

export function getJBangConfiguration(): WorkspaceConfiguration {
return workspace.getConfiguration('jbang');
}

export function setShowTemplateDescriptions(value: boolean): Thenable<void> {
return setJBangConfig(SHOW_TEMPLATE_DESC, value);
}

export function setJBangConfig<T>(configName: string, value: T): Thenable<void> {
const info = getJBangConfiguration().inspect(configName);
let scope = ConfigurationTarget.Global;
if (info?.workspaceValue !== undefined) {
scope = ConfigurationTarget.Workspace
} else if (info?.workspaceFolderValue !== undefined) {
scope = ConfigurationTarget.WorkspaceFolder;
}
return getJBangConfiguration().update(configName, value, scope);
}
}

22 changes: 21 additions & 1 deletion src/JBangExec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
import path = require('path');
import * as os from 'os';
import { workspace, WorkspaceConfiguration } from 'vscode';
import { JBangConfig } from './JBangConfig';

function isWin():boolean {
return /^win/.test(process.platform);
}

export function jbang():string {
return isWin()?"jbang.cmd":"jbang";
let home = JBangConfig.getJBangHome();
let dir = "";
if (home) {
if (home.startsWith('~')) {
home = path.resolve(os.homedir() , home.substring(2, home.length));
}
dir = path.resolve(home, 'bin');
}
const jbangExec = (isWin()?"jbang.cmd":"jbang");
const fullPath = path.resolve(dir, jbangExec);
console.log("Using JBang from "+fullPath);
return fullPath;
}

export function getJBangConfiguration(): WorkspaceConfiguration {
return workspace.getConfiguration('jbang');
}
3 changes: 2 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ExtensionContext } from "vscode";
import { Assets } from "./Assets";
import CodeLensProvider from "./CodeLensProvider";
import CommandManager from "./CommandManager";
import EditorListener from "./EditorListener";
import JBangRunner from "./JBangRunner";

export function activate(context: ExtensionContext) {

JBangRunner.initialize(context);
Assets.initialize(context);
CommandManager.initialize(context);
EditorListener.initialize(context);
CodeLensProvider.initialize(context);
Expand Down
121 changes: 104 additions & 17 deletions src/wizards/JBangScriptWizard.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import * as fs from "fs";
import { commands, OpenDialogOptions, Uri, window, workspace } from "vscode";
import { commands, OpenDialogOptions, QuickPickItem, Uri, window, workspace } from "vscode";
import { Assets } from "../Assets";
import { JBangConfig } from "../JBangConfig";
import { runWithStandardMode } from "../utils/javaExtension";
import { MultiStepInput } from "./multiStepsUtils";
import { generateScript, getTemplates } from "./templateExec";
import { JBangTemplate } from "./JBangTemplate";
import { MultiStepInput, QuickInputButtonWithCallback, QuickPickParameters } from "./multiStepsUtils";
import { generateScript, listTemplates } from "./templateExec";
import { ScriptGenState } from "./wizardState";
import path = require("path");

const CUSTOM_TEMPLATE = {
label: "$(pencil) Custom template ...",
detail: "Manually set a custom template"
} as QuickPickItem

const DEFAULT_TOTAL_STEPS = 3;

export default class JBangScriptWizard {

constructor() { }
private showDescription: boolean;
private templates: JBangTemplate[]|undefined;

constructor() {
this.showDescription = JBangConfig.isShowTemplateDescriptions();
}

public static async open() {
const wizard = new JBangScriptWizard();
Expand All @@ -17,7 +32,7 @@ export default class JBangScriptWizard {

private async run() {
const state: Partial<ScriptGenState> = {
totalSteps: 3,
totalSteps: DEFAULT_TOTAL_STEPS,
};
await MultiStepInput.run(input => this.inputTemplate(input, state));
if (!state.scriptName) {
Expand All @@ -37,33 +52,95 @@ export default class JBangScriptWizard {
setTimeout(() => {
commands.executeCommand("jbang.synchronize", uri);
}, 1000);
},"Synchronize JBang");
}, "Synchronize JBang");
}
} catch (e: any) {
window.showErrorMessage(e.message);
}
}

private async inputTemplate(input: MultiStepInput, state: Partial<ScriptGenState>) {
const templates = await getTemplates();
const template = await input.showQuickPick({
title: "Select a template",
step: 1,
state.totalSteps = DEFAULT_TOTAL_STEPS;

let selectedTemplate: QuickPickItem | undefined;
do {
const templates: QuickPickItem[] = [];
templates.push(CUSTOM_TEMPLATE);

const jbangTemplates = (await this.getTemplates()).map(t => this.asItem(t));
templates.push(...jbangTemplates);
selectedTemplate = await input.showQuickPick<QuickPickItem, QuickPickParameters<QuickPickItem>>({
title: "Select a template",
step: 1,
totalSteps: state.totalSteps,
items: templates,
placeholder: "Select a template",
buttons: [this.getInfoButton()],
configChanges: [
{
configName: "jbang." + JBangConfig.SHOW_TEMPLATE_DESC,
callback: () => this.showDescription = JBangConfig.isShowTemplateDescriptions()
}
]
});

if (selectedTemplate) {
if (selectedTemplate?.label === CUSTOM_TEMPLATE.label) {
return (input: MultiStepInput) => this.inputCustomTemplate(input, state);
}

state.template = selectedTemplate.label;

return (input: MultiStepInput) => this.inputScriptName(input, state);
}
} while (!selectedTemplate)
}

private asItem(template: JBangTemplate): QuickPickItem {
return {
label: template.label,
description: this.showDescription ? template.description : undefined
}
}

private async getTemplates(): Promise<JBangTemplate[]> {
if (!this.templates) {
this.templates = await listTemplates();
}
return this.templates;
}

private getInfoButton(): QuickInputButtonWithCallback {
const darkThemeIcon = Assets.get('buttons/info_dark_theme.svg');
const lightThemeIcon = Assets.get('/buttons/info_light_theme.svg');
return {
iconPath: { light: lightThemeIcon, dark: darkThemeIcon },
tooltip: 'Toggle template descriptions',
callback: () => {
this.showDescription = !this.showDescription;
JBangConfig.setShowTemplateDescriptions(this.showDescription);
}
} as QuickInputButtonWithCallback;
}

private async inputCustomTemplate(input: MultiStepInput, state: Partial<ScriptGenState>) {
state.totalSteps = DEFAULT_TOTAL_STEPS + 1;
state.template = await input.showInputBox({
title: "Custom template",
step: 2,
totalSteps: state.totalSteps,
items: templates,
placeholder: "Select a template",
value: state.template || "",
prompt: "Enter a template name",
validate: validateTemplateName
});
if (template) {
state.template = template.label;
}

return (input: MultiStepInput) => this.inputScriptName(input, state);
}

private async inputScriptName(input: MultiStepInput, state: Partial<ScriptGenState>) {
state.scriptName = await input.showInputBox({
title: "Enter a script name",
step: 2,
step: state.totalSteps! - 1,
totalSteps: state.totalSteps,
value: state.scriptName || "",
prompt: "Enter a script name",
Expand All @@ -86,14 +163,24 @@ async function validateName(name: string): Promise<string | undefined> {
return undefined;
}

async function validateTemplateName(name: string): Promise<string | undefined> {
if (!name) {
return "Template name is required";
}
if (name.includes(" ")) {
return "Template name cannot contain spaces";
}
return undefined;
}

async function getTargetDirectory(fileName: string) {
const MESSAGE_EXISTING_FILE = `'${fileName}' already exists in selected directory.`;
const LABEL_CHOOSE_FOLDER = 'Generate Here';
const OPTION_OVERWRITE = 'Overwrite';
const OPTION_CHOOSE_NEW_DIR = 'Choose new directory';

const defaultDirectoryUri = workspace.workspaceFolders ? workspace.workspaceFolders[0].uri : undefined;

let directory: Uri | undefined;
if (defaultDirectoryUri) {
const defaultDirectory = defaultDirectoryUri.fsPath;
Expand Down
14 changes: 8 additions & 6 deletions src/wizards/templateExec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { executeCommand } from "../utils/cpUtils";
import { JBangTemplate } from "./JBangTemplate";
import { ScriptGenState } from "./wizardState";

export async function getTemplates(): Promise<JBangTemplate[]> {
export async function listTemplates(): Promise<JBangTemplate[]> {
const data = await executeCommand(jbang(), ["template", "list"], {
shell: true,
env: {
Expand All @@ -16,11 +16,13 @@ export async function getTemplates(): Promise<JBangTemplate[]> {
const lines = data.toString().split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const template = line.split('=');
templates.push({
label: template[0],
description: template[1]
});
if (line.indexOf("=") > 0) {
const template = line.split('=');
templates.push({
label: template[0],
description: template[1]
});
}
}
return templates;
}
Expand Down

0 comments on commit 644d5b1

Please sign in to comment.