Skip to content

Commit

Permalink
feat: run recipe
Browse files Browse the repository at this point in the history
  • Loading branch information
nefrob committed Oct 6, 2024
1 parent 772eac5 commit 72ed685
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 43 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Unicode codepoint escaped characters in strings from `just` release 1.36.0
- Unexport keyword from `just` release 1.29.0
- VSCode command to run recipes

## [0.5.3] - 2024-08-14

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Completed:
- [x] Add snapshot testing
- [x] Publish to [open source marketplaces](https://open-vsx.org/)
- [x] Format on save
- [x] Run recipe
### Beyond
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@
{
"command": "vscode-just.formatDocument",
"title": "Just: Format Document"
},
{
"command": "vscode-just.runRecipe",
"title": "Just: Run Recipe"
}
]
},
Expand All @@ -104,5 +108,9 @@
"vscode-tmgrammar-test": "^0.1.3",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@types/yargs-parser": "^21.0.3",
"yargs-parser": "^21.1.1"
}
}
1 change: 1 addition & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const EXTENSION_NAME = 'vscode-just';
65 changes: 22 additions & 43 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,17 @@
import { spawn } from 'child_process';
import * as vscode from 'vscode';

import { EXTENSION_NAME } from './const';
import { formatWithExecutable } from './format';
import Logger from './logger';
import { runRecipeCommand } from './recipe';

const EXTENSION_NAME = 'vscode-just';

let logger: Logger;

const formatWithExecutable = (fsPath: string) => {
const justPath =
(vscode.workspace.getConfiguration(EXTENSION_NAME).get('justPath') as string) ||
'just';
const args = ['-f', fsPath, '--fmt', '--unstable'];

const childProcess = spawn(justPath, args);
childProcess.stdout.on('data', (data: string) => {
logger.info(data);
});
childProcess.stderr.on('data', (data: string) => {
// TODO: successfully formatted documents also log to stderr
// so treat everything as info for now
logger.info(data);
// showErrorWithLink('Error formatting document.');
});
childProcess.on('close', (code) => {
console.debug(`just --fmt exited with ${code}`);
});
};

const showErrorWithLink = (message: string) => {
const outputButton = 'Output';
vscode.window
.showErrorMessage(message, outputButton)
.then((selection) => selection === outputButton && logger.show());
};

vscode.workspace.onWillSaveTextDocument((event) => {
if (vscode.workspace.getConfiguration(EXTENSION_NAME).get('formatOnSave')) {
formatWithExecutable(event.document.uri.fsPath);
}
});
export let LOGGER: Logger;

export const activate = (context: vscode.ExtensionContext) => {
console.debug(`${EXTENSION_NAME} activated`);
logger = new Logger(EXTENSION_NAME);
LOGGER = new Logger(EXTENSION_NAME);

const disposable = vscode.commands.registerCommand(
const formatDisposable = vscode.commands.registerCommand(
`${EXTENSION_NAME}.formatDocument`,
() => {
const editor = vscode.window.activeTextEditor;
Expand All @@ -54,13 +20,26 @@ export const activate = (context: vscode.ExtensionContext) => {
}
},
);
context.subscriptions.push(formatDisposable);

context.subscriptions.push(disposable);
const runRecipeDisposable = vscode.commands.registerCommand(
`${EXTENSION_NAME}.runRecipe`,
async () => {
runRecipeCommand();
},
);
context.subscriptions.push(runRecipeDisposable);
};

export const deactivate = () => {
console.debug(`${EXTENSION_NAME} deactivated`);
if (logger) {
logger.dispose();
if (LOGGER) {
LOGGER.dispose();
}
};

vscode.workspace.onWillSaveTextDocument((event) => {
if (vscode.workspace.getConfiguration(EXTENSION_NAME).get('formatOnSave')) {
formatWithExecutable(event.document.uri.fsPath);
}
});
26 changes: 26 additions & 0 deletions src/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { spawn } from 'child_process';
import * as vscode from 'vscode';

import { EXTENSION_NAME } from './const';
import { LOGGER } from './extension';

export const formatWithExecutable = (fsPath: string) => {
const justPath =
(vscode.workspace.getConfiguration(EXTENSION_NAME).get('justPath') as string) ||
'just';
const args = ['-f', fsPath, '--fmt', '--unstable'];

const childProcess = spawn(justPath, args);
childProcess.stdout.on('data', (data: string) => {
LOGGER.info(data);
});
childProcess.stderr.on('data', (data: string) => {
// TODO: successfully formatted documents also log to stderr
// so treat everything as info for now
LOGGER.info(data);
// showErrorWithLink('Error formatting document.');
});
childProcess.on('close', (code) => {
console.debug(`just --fmt exited with ${code}`);
});
};
120 changes: 120 additions & 0 deletions src/recipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { exec, spawn } from 'child_process';
import { promisify } from 'util';
import * as vscode from 'vscode';
import yargsParser from 'yargs-parser';

import { EXTENSION_NAME } from './const';
import { LOGGER } from './extension';
import { RecipeParameterKind, RecipeParsed, RecipeResponse } from './types';
import { workspaceRoot } from './utils';

const asyncExec = promisify(exec);

export const runRecipeCommand = async () => {
const recipes = await getRecipes();
if (!recipes.length) return;

const choices: vscode.QuickPickItem[] = recipes
.map((r) => ({
label: r.name,
description: r.doc,
detail: r.groups.length ? `Groups: ${r.groups.sort().join(', ')}` : '',
}))
.sort((a, b) => a.label.localeCompare(b.label));

const recipeToRun = await vscode.window.showQuickPick(choices, {
placeHolder: 'Select a recipe to run',
});
if (!recipeToRun) return;

const recipe = recipes.find((r) => r.name === recipeToRun?.label);
if (!recipe) {
const errorMsg = 'Failed to find recipe';
vscode.window.showErrorMessage(errorMsg);
LOGGER.error(errorMsg);
return;
}

let args: string | undefined = '';
if (recipe.parameters.length) {
args = await vscode.window.showInputBox({
placeHolder: `Enter arguments: ${paramsToString(recipe.parameters)}`,
});
}

if (args === undefined) return;
runRecipe(recipe, yargsParser(args));
};

const getRecipes = async (): Promise<RecipeParsed[]> => {
const justPath =
(vscode.workspace.getConfiguration(EXTENSION_NAME).get('justPath') as string) ||
'just';

const cmd = `${justPath} --dump --dump-format=json`;
try {
const { stdout, stderr } = await asyncExec(cmd, { cwd: workspaceRoot() ?? '~' });

if (stderr) {
vscode.window.showErrorMessage('Failed to fetch recipes.');
LOGGER.error(stderr);
return [];
}

return parseRecipes(stdout);
} catch (error) {
if (error instanceof Error) {
vscode.window.showErrorMessage('Failed to fetch recipes.');
LOGGER.error(error.message);
} else {
vscode.window.showErrorMessage('Failed to fetch recipes.');
LOGGER.error('An unknown error occurred.');
}

return [];
}
};

const parseRecipes = (output: string): RecipeParsed[] => {
return (Object.values(JSON.parse(output).recipes) as RecipeResponse[])
.filter((r) => !r.private && !r.attributes.some((attr) => attr === 'private'))
.map((r) => ({
name: r.name,
doc: r.doc,
parameters: r.parameters,
groups: r.attributes
.filter((attr) => typeof attr === 'object' && attr.group)
.map((attr) => (attr as Record<string, string>).group),
}));
};

const paramsToString = (params: RecipeParsed['parameters']): string => {
return params
.sort((a, b) =>
a.kind === RecipeParameterKind.PLUS ? 1 : a.name.localeCompare(b.name),
)
.map((p) => {
let formatted = `${p.kind === RecipeParameterKind.PLUS ? '+' : ''}${p.name}`;
if (p.default != null) formatted += `=${p.default}`;
return formatted;
})
.join(' ');
};

const runRecipe = async (recipe: RecipeParsed, optionalArgs: yargsParser.Arguments) => {
const justPath =
(vscode.workspace.getConfiguration(EXTENSION_NAME).get('justPath') as string) ||
'just';
const args = [recipe.name, ...optionalArgs._.map((arg) => arg.toString())];

const childProcess = spawn(justPath, args, { cwd: workspaceRoot() ?? '~' });
childProcess.stdout.on('data', (data: string) => {
LOGGER.info(data);
});
childProcess.stderr.on('data', (data: string) => {
// TODO: successfully run recipes also log to stderr
// so treat everything as info for now
LOGGER.info(data);
// showErrorWithLink('Error running recipe.');
});
};
27 changes: 27 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export enum RecipeParameterKind {
SINGULAR = 'singular',
PLUS = 'plus',
}

export interface RecipeParameter {
default: string;
kind: RecipeParameterKind;
name: string;
[key: string]: unknown;
}

export interface RecipeResponse {
name: string;
doc: string;
parameters: RecipeParameter[];
attributes: (Record<string, string> | string)[];
private: boolean;
[key: string]: unknown;
}

export interface RecipeParsed {
name: string;
doc: string;
parameters: Pick<RecipeParameter, 'default' | 'kind' | 'name'>[];
groups: string[];
}
17 changes: 17 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as vscode from 'vscode';

import { LOGGER } from './extension';

export const showErrorWithLink = (message: string) => {
const outputButton = 'Output';
vscode.window
.showErrorMessage(message, outputButton)
.then((selection) => selection === outputButton && LOGGER.show());
};

export const workspaceRoot = (): string | null => {
const workspaceFolders = vscode.workspace.workspaceFolders;
return workspaceFolders && workspaceFolders.length > 0
? workspaceFolders[0].uri.fsPath
: null;
};
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.93.0.tgz#1cd7573e0272aef9c357bafc635b6177c154013e"
integrity sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==

"@types/yargs-parser@^21.0.3":
version "21.0.3"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==

"@typescript-eslint/[email protected]":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz#b16d3cf3ee76bf572fdf511e79c248bdec619ea3"
Expand Down

0 comments on commit 72ed685

Please sign in to comment.