Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Requires a **JDK 11+** with `jshell` on your `PATH`. Verify your setup with `psj
```bash
psjava example.psjava # run the file
psjava example.psjava --debug # run, and print the elapsed time at the end
psjava doctor # check that jshell is available
psjava doctor # check jshell and the editor syntax highlighting
psjava highlight install # enable .psjava syntax highlighting in your editors
```

A `.psjava` file is just Java:
Expand All @@ -43,6 +44,15 @@ print(java.util.List.of("a", "b")); // [a, b]

`System.out.println(...)` keeps working as usual.

## Syntax highlighting

Since a `.psjava` file is plain Java, `psjava highlight install` just tells your editor to treat `*.psjava` as Java — no extension or plugin to maintain:

- **VSCode** — adds `"*.psjava": "java"` to your user `settings.json`.
- **IntelliJ** — maps `*.psjava` to the Java file type in `filetypes.xml`.

It detects each editor's config folder automatically (Windows) and asks for the path when it can't find one. Reopen the editor afterwards for the highlighting to kick in. `psjava doctor` reports, per editor, whether the highlighting is already set up.

## How it works

`psjava` reads your file, prepends the `print` overloads, and pipes the result straight into `jshell -s`. The only change made to your source is removing the Windows BOM, which `jshell` chokes on. That's it — plain Java into JShell.
Expand Down
90 changes: 89 additions & 1 deletion e2e-test/run.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { execFileSync, spawnSync } from 'child_process';
import { mkdtempSync, writeFileSync, existsSync } from 'fs';
import { mkdtempSync, writeFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';

Expand Down Expand Up @@ -89,4 +89,92 @@ describe.skipIf(!ready)('psjava e2e (jshell real)', () => {
expect(res.status).not.toBe(0);
expect(res.stderr).toContain('only runs .psjava files');
});

// APPDATA temporário com as pastas pré-criadas → install roda sem prompt e não toca a config real.
function fakeAppData() {
const appData = mkdtempSync(join(tmpdir(), 'psjava-appdata-'));
mkdirSync(join(appData, 'Code', 'User'), { recursive: true });
mkdirSync(join(appData, 'JetBrains', 'IntelliJIdea2024.1'), { recursive: true });
return appData;
}

// APPDATA vazio → nenhuma IDE encontrada (dispara o prompt no install, "não encontrado" no doctor).
const emptyAppData = () => mkdtempSync(join(tmpdir(), 'psjava-empty-'));

it('highlight install associa *.psjava ao Java no VSCode e IntelliJ', () => {
const appData = fakeAppData();
const env = { ...process.env, APPDATA: appData };
const res = spawnSync('node', [CLI, 'highlight', 'install'], { encoding: 'utf8', env });
expect(res.status).toBe(0);
expect(readFileSync(join(appData, 'Code', 'User', 'settings.json'), 'utf8')).toContain(
'"*.psjava": "java"',
);
expect(
readFileSync(join(appData, 'JetBrains', 'IntelliJIdea2024.1', 'options', 'filetypes.xml'), 'utf8'),
).toContain('pattern="*.psjava"');
});

it('highlight install é idempotente (rodar 2x mantém uma associação válida)', () => {
const appData = fakeAppData();
const env = { ...process.env, APPDATA: appData };
spawnSync('node', [CLI, 'highlight', 'install'], { encoding: 'utf8', env });
const res = spawnSync('node', [CLI, 'highlight', 'install'], { encoding: 'utf8', env });
expect(res.status).toBe(0);
const settings = readFileSync(join(appData, 'Code', 'User', 'settings.json'), 'utf8');
expect(JSON.parse(settings)['files.associations']['*.psjava']).toBe('java');
expect(settings.match(/\*\.psjava/g)?.length).toBe(1); // sem duplicar
});

it('highlight install pula a IDE quando não acha e o usuário dá Enter', () => {
const appData = emptyAppData();
const env = { ...process.env, APPDATA: appData };
const res = spawnSync('node', [CLI, 'highlight', 'install'], {
encoding: 'utf8',
env,
input: '\n\n', // VSCode e IntelliJ: Enter = pular
});
expect(res.status).toBe(0);
expect(res.stdout).toContain('pulado');
});

it('highlight install usa o caminho informado no prompt quando não acha', () => {
const appData = emptyAppData();
const vsDir = mkdtempSync(join(tmpdir(), 'vs-'));
const ijDir = mkdtempSync(join(tmpdir(), 'ij-'));
const env = { ...process.env, APPDATA: appData };
const res = spawnSync('node', [CLI, 'highlight', 'install'], {
encoding: 'utf8',
env,
input: `${vsDir}\n${ijDir}\n`,
});
expect(res.status).toBe(0);
expect(readFileSync(join(vsDir, 'settings.json'), 'utf8')).toContain('"*.psjava": "java"');
expect(readFileSync(join(ijDir, 'options', 'filetypes.xml'), 'utf8')).toContain('pattern="*.psjava"');
});

it('doctor reporta o realce configurado depois do install (e sai com 0)', () => {
const appData = fakeAppData();
const env = { ...process.env, APPDATA: appData };
spawnSync('node', [CLI, 'highlight', 'install'], { encoding: 'utf8', env });
const res = spawnSync('node', [CLI, 'doctor'], { encoding: 'utf8', env });
expect(res.status).toBe(0);
expect(res.stdout).toContain('VSCode');
expect(res.stdout).toMatch(/realce configurado/);
});

it('doctor reporta realce ausente quando a IDE existe sem config (aviso, exit 0)', () => {
const appData = fakeAppData(); // pastas existem, mas sem install
const env = { ...process.env, APPDATA: appData };
const res = spawnSync('node', [CLI, 'doctor'], { encoding: 'utf8', env });
expect(res.status).toBe(0);
expect(res.stdout).toMatch(/ausente/);
});

it('doctor reporta não encontrado quando não há IDEs (exit 0)', () => {
const appData = emptyAppData();
const env = { ...process.env, APPDATA: appData };
const res = spawnSync('node', [CLI, 'doctor'], { encoding: 'utf8', env });
expect(res.status).toBe(0);
expect(res.stdout).toContain('não encontrado');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Syntax highlight de .psjava no IntelliJ e VSCode

## Objetivo
Dar ao usuário um jeito de 1 comando para habilitar realce de sintaxe em
arquivos `.psjava` no IntelliJ e no VSCode, e fazer o `psjava doctor` reportar
se esse realce já está configurado.

## Comportamento esperado
- Um comando (ex.: `psjava highlight install`) detecta as IDEs instaladas no PC,
configura o realce em cada uma e reporta o que fez.
- Quando não acha o diretório de config de uma IDE, pergunta o caminho ali mesmo
via CLI (não falha calado).
- O `psjava doctor` passa a checar, por IDE, se o realce está configurado —
como já faz com o JDK/jshell.

## Fora de escopo
- Publicar extensão em marketplaces (VSCode Marketplace / JetBrains).
- Editores além de IntelliJ e VSCode.
- Definir uma gramática própria de `.psjava` (é Java puro; reaproveitar o realce
Java existente).
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Syntax highlight de .psjava no IntelliJ e VSCode — Delta

## Added
- Comando `psjava highlight install`: associa `*.psjava` à linguagem Java no VSCode
(`files.associations` no settings.json) e no IntelliJ (mapping no filetypes.xml).
Idempotente, preserva config existente; quando não acha a pasta da IDE, pergunta
o caminho via CLI (Enter pula).
- Núcleo puro `src/core/highlight.ts` (paths Windows, merges, estado por IDE) e
asker de stdin resiliente a EOF (`src/core/prompt.ts`).

## Changed
- `psjava doctor` passa a reportar, por IDE, o estado do realce
(✓ configurado / ⚠ ausente / – não encontrado). Realce ausente é **aviso** e
mantém exit 0; só o jshell faltando continua sendo erro (exit 1).
- README ganha a seção "Syntax highlighting" e o comando no Usage.

## Out of scope
- macOS/Linux (apenas paths de config do Windows).
- Extensão/plugin em marketplace e gramática `.psjava` própria.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Grill Me
- [x] Como configurar o realce? — Associar `*.psjava` à linguagem Java existente (reaproveita o realce Java; sem extensão/plugin próprio).
- [x] Quais SOs suportar? — Windows primeiro; macOS/Linux ficam para depois.
- [x] Doctor com realce ausente? — Reporta como aviso e mantém exit 0; só o JDK é requisito duro (exit 1).
- [x] Como detectar a IDE? — Pela pasta de config (VSCode `%APPDATA%/Code/User/`, IntelliJ `%APPDATA%/JetBrains/<Produto><versão>/`); se não achar, pergunta o caminho via CLI.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Syntax highlight de .psjava no IntelliJ e VSCode

## Summary
Como `.psjava` é Java puro, um comando `psjava highlight install` apenas associa
`*.psjava` à linguagem Java já existente no VSCode e no IntelliJ — sem extensão
ou plugin próprio. Se não achar a pasta de config de uma IDE, pergunta o caminho
na hora. O `psjava doctor` passa a reportar, por IDE, se a associação existe.

## Technical detail
- **Núcleo puro** `src/core/highlight.ts`: descobre paths de config (Windows),
decide o estado por IDE (`configured` / `missing` / `ide-not-found`) e aplica a
associação. Lógica pura e testável (mesmo padrão de `buildSession`).
- **VSCode**: editar `%APPDATA%/Code/User/settings.json`, garantindo
`"files.associations": { "*.psjava": "java" }` (merge sem destruir o resto;
VSCode já traz gramática Java embutida).
- **IntelliJ**: editar `%APPDATA%/JetBrains/<Produto><versão>/options/filetypes.xml`,
adicionando `<mapping pattern="*.psjava" type="JAVA"/>` no `extensionMap`
(idempotente). Há 1+ pastas de produto/versão — aplicar em todas as achadas.
- **Detecção**: pasta de config existe → configura; não existe → `AskUserQuestion`/
prompt CLI pedindo o caminho (ou pular essa IDE).
- **Comando**: subcomando `highlight install` no commander (`src/commands/highlight.ts`),
registrado no `cli.ts` ao lado de `doctor`.
- **Doctor**: `runDoctor` chama o checador puro e imprime por IDE
(✓ configurado / ⚠ ausente / – não encontrada). Ausente é **aviso**, não muda
o exit code; só jshell faltando mantém exit 1.

## Scope
### In
- Subcomando `psjava highlight install` para VSCode + IntelliJ no Windows.
- `psjava doctor` reportando o estado do realce por IDE (aviso, não falha).
- Prompt via CLI quando a pasta de config não é encontrada.
- Unit para a lógica pura; e2e do subcomando no nível certo.
### Out
- macOS/Linux (paths de config dos outros SOs).
- Publicar extensão/plugin em marketplace.
- Gramática `.psjava` própria.
- Outros editores.

## Subtasks
- [x] Núcleo `highlight.ts`: paths de config no Windows + função pura que decide o estado por IDE
- [x] VSCode: merge de `files.associations` em settings.json (idempotente) + unit
- [x] IntelliJ: merge de `*.psjava→JAVA` em filetypes.xml (idempotente) + unit
- [x] Prompt CLI para informar o caminho quando a pasta de config não é achada
- [x] Comando `highlight install` no commander e registro no `cli.ts`
- [x] `doctor` reporta o estado do realce por IDE (aviso, mantém exit 0)
- [x] e2e do subcomando + atualizar README/docs
9 changes: 9 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Command } from 'commander';
import { createRequire } from 'module';
import { runFile } from './commands/run.js';
import { runDoctor } from './commands/doctor.js';
import { runHighlightInstall } from './commands/highlight.js';

// Versão vem do package.json (fonte única) — em dist/cli.js, '../package.json' é a raiz do pacote.
const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
Expand All @@ -19,6 +20,14 @@ export function buildProgram(): Command {
});

program.command('doctor').description('Check the JDK (jshell)').action(runDoctor);

program
.command('highlight')
.description('Syntax highlight de .psjava nas IDEs')
.command('install')
.description('Associa *.psjava ao Java no VSCode e IntelliJ')
.action(runHighlightInstall);

return program;
}

Expand Down
4 changes: 3 additions & 1 deletion src/commands/doctor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import chalk from 'chalk';
import { resolveJshell } from '../core/jdk.js';
import { reportHighlightStatus } from './highlight.js';

export async function runDoctor(): Promise<void> {
try {
await resolveJshell();
console.log(chalk.green('✓ jshell found — ready to run .psjava'));
} catch (err) {
console.log(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
process.exitCode = 1;
process.exitCode = 1; // só o JDK faltando é erro duro
}
await reportHighlightStatus(); // realce é aviso, não muda o exit
}
134 changes: 134 additions & 0 deletions src/commands/highlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { readFile, writeFile, mkdir, readdir, access } from 'fs/promises';
import { join, dirname, basename } from 'path';
import chalk from 'chalk';
import {
vscodeSettingsPath,
jetbrainsBaseDir,
filetypesPath,
mergeVscodeAssociation,
mergeIntellijMapping,
vscodeHighlightState,
intellijHighlightState,
type IdeState,
} from '../core/highlight.js';
import { createAsker, type Ask } from '../core/prompt.js';

async function exists(p: string): Promise<boolean> {
try {
await access(p);
return true;
} catch {
return false;
}
}

async function readOrNull(p: string): Promise<string | null> {
try {
return await readFile(p, 'utf8');
} catch {
return null; // arquivo ainda não existe — merge cria do zero
}
}

async function writeMerged(file: string, content: string): Promise<void> {
await mkdir(dirname(file), { recursive: true });
await writeFile(file, content, 'utf8');
}

/** Pasta `User` do VSCode, ou null se não existe (caller decide perguntar/pular). */
export async function findVscodeUserDir(): Promise<string | null> {
const dir = dirname(vscodeSettingsPath());
return (await exists(dir)) ? dir : null;
}

/** Pastas de config dos produtos IntelliJ achados em %APPDATA%/JetBrains. */
export async function findIntellijDirs(): Promise<string[]> {
const base = jetbrainsBaseDir();
if (!(await exists(base))) return [];
const entries = await readdir(base, { withFileTypes: true });
// ponytail: filtra só produtos IntelliJ; JetBrains/ também tem Toolbox, consentOptions etc.
return entries
.filter((d) => d.isDirectory() && /^(IntelliJIdea|IdeaIC)/i.test(d.name))
.map((d) => join(base, d.name));
}

/** Configura o VSCode. Devolve o settings.json escrito, ou null se pulado. */
async function installVscode(ask: Ask): Promise<string | null> {
let userDir = await findVscodeUserDir();
if (!userDir) {
const ans = await ask(
`VSCode não encontrado em ${dirname(vscodeSettingsPath())}.\n Caminho da pasta User do VSCode (Enter pula): `,
);
if (!ans) return null;
userDir = ans;
}
const settingsPath = join(userDir, 'settings.json');
await writeMerged(settingsPath, mergeVscodeAssociation(await readOrNull(settingsPath)));
return settingsPath;
}

/** Configura todo produto IntelliJ achado. Devolve os filetypes.xml escritos. */
async function installIntellij(ask: Ask): Promise<string[]> {
let dirs = await findIntellijDirs();
if (dirs.length === 0) {
const ans = await ask(
`IntelliJ não encontrado em ${jetbrainsBaseDir()}.\n Caminho da pasta de config do IntelliJ (Enter pula): `,
);
if (!ans) return [];
dirs = [ans];
}
const written: string[] = [];
for (const dir of dirs) {
const file = filetypesPath(dir);
await writeMerged(file, mergeIntellijMapping(await readOrNull(file)));
written.push(file);
}
return written;
}

export async function runHighlightInstall(): Promise<void> {
const { ask, close } = createAsker();
try {
const vscode = await installVscode(ask);
console.log(vscode ? chalk.green(`✓ VSCode: ${vscode}`) : chalk.yellow('– VSCode: pulado'));

const intellij = await installIntellij(ask);
if (intellij.length === 0) {
console.log(chalk.yellow('– IntelliJ: pulado'));
} else {
for (const f of intellij) console.log(chalk.green(`✓ IntelliJ: ${f}`));
}
console.log(chalk.dim('Reabra o editor para o realce valer.'));
} finally {
close(); // libera o stdin pra o processo conseguir sair
}
}

function printState(label: string, state: IdeState): void {
if (state === 'configured') console.log(chalk.green(`✓ ${label}: realce configurado`));
else if (state === 'missing')
console.log(chalk.yellow(`⚠ ${label}: realce ausente (rode: psjava highlight install)`));
else console.log(chalk.dim(`– ${label}: não encontrado`));
}

/** Reporta o estado do realce por IDE. Apenas informa — nunca mexe no exit code. */
export async function reportHighlightStatus(): Promise<void> {
// IDE presente sem o arquivo de config = ausente (a achar a pasta já prova que a IDE existe).
const userDir = await findVscodeUserDir();
if (!userDir) {
printState('VSCode', 'ide-not-found');
} else {
const settings = await readOrNull(join(userDir, 'settings.json'));
printState('VSCode', settings === null ? 'missing' : vscodeHighlightState(settings));
}

const dirs = await findIntellijDirs();
if (dirs.length === 0) {
printState('IntelliJ', 'ide-not-found');
} else {
for (const dir of dirs) {
const xml = await readOrNull(filetypesPath(dir));
printState(`IntelliJ (${basename(dir)})`, xml === null ? 'missing' : intellijHighlightState(xml));
}
}
}
Loading