diff --git a/README.md b/README.md index 199befd..6ef43f0 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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. diff --git a/e2e-test/run.e2e.test.ts b/e2e-test/run.e2e.test.ts index 36e6196..edb20a6 100644 --- a/e2e-test/run.e2e.test.ts +++ b/e2e-test/run.e2e.test.ts @@ -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'; @@ -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'); + }); }); diff --git a/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/.issue b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/.issue new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/.issue @@ -0,0 +1 @@ +9 diff --git a/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/brief.md b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/brief.md new file mode 100644 index 0000000..ff211e4 --- /dev/null +++ b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/brief.md @@ -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). diff --git a/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/delta-spec.md b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/delta-spec.md new file mode 100644 index 0000000..119485a --- /dev/null +++ b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/delta-spec.md @@ -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. diff --git a/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/questions.md b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/questions.md new file mode 100644 index 0000000..6e21ce9 --- /dev/null +++ b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/questions.md @@ -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//`); se não achar, pergunta o caminho via CLI. diff --git a/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/refine.md b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/refine.md new file mode 100644 index 0000000..0a2be41 --- /dev/null +++ b/pscode/changes/archive/2026-06-28-feat-syntax-highlight-ide/refine.md @@ -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//options/filetypes.xml`, + adicionando `` 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 diff --git a/src/cli.ts b/src/cli.ts index 8e9859d..c15550a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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 }; @@ -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; } diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 907d122..8b8fccf 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import { resolveJshell } from '../core/jdk.js'; +import { reportHighlightStatus } from './highlight.js'; export async function runDoctor(): Promise { try { @@ -7,6 +8,7 @@ export async function runDoctor(): Promise { 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 } diff --git a/src/commands/highlight.ts b/src/commands/highlight.ts new file mode 100644 index 0000000..724651a --- /dev/null +++ b/src/commands/highlight.ts @@ -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 { + try { + await access(p); + return true; + } catch { + return false; + } +} + +async function readOrNull(p: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + // 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)); + } + } +} diff --git a/src/core/highlight.ts b/src/core/highlight.ts new file mode 100644 index 0000000..ba3f2c0 --- /dev/null +++ b/src/core/highlight.ts @@ -0,0 +1,93 @@ +import { join } from 'path'; + +// .psjava é Java puro: o realce é só associar o padrão à linguagem Java que a IDE já tem. +export type IdeState = 'configured' | 'missing' | 'ide-not-found'; + +/** %APPDATA%/Code/User/settings.json — onde o VSCode guarda as settings do usuário (Windows). */ +export function vscodeSettingsPath(env = process.env): string { + return join(env.APPDATA ?? '', 'Code', 'User', 'settings.json'); +} + +/** %APPDATA%/JetBrains — raiz com uma pasta de config por produto/versão do IntelliJ (Windows). */ +export function jetbrainsBaseDir(env = process.env): string { + return join(env.APPDATA ?? '', 'JetBrains'); +} + +/** /options/filetypes.xml — onde o IntelliJ guarda as associações de tipo. */ +export function filetypesPath(productDir: string): string { + return join(productDir, 'options', 'filetypes.xml'); +} + +/** Estado do realce no VSCode a partir do settings.json (null = VSCode não encontrado). */ +export function vscodeHighlightState(settings: string | null): IdeState { + if (settings === null) return 'ide-not-found'; + // ponytail: regex em vez de parse JSONC — settings.json aceita comentários; o check é só leitura + return /"\*\.psjava"\s*:\s*"java"/.test(settings) ? 'configured' : 'missing'; +} + +/** Estado do realce no IntelliJ a partir do filetypes.xml (null = produto não encontrado). */ +export function intellijHighlightState(filetypes: string | null): IdeState { + if (filetypes === null) return 'ide-not-found'; + return /pattern="\*\.psjava"\s+type="JAVA"/.test(filetypes) ? 'configured' : 'missing'; +} + +/** + * Garante `"*.psjava": "java"` em `files.associations`, preservando o resto do settings. + * Idempotente. Lança se o settings.json não for JSON parseável (ex.: tem comentários), + * pra nunca corromper o arquivo — nesse caso o usuário adiciona a linha à mão. + */ +export function mergeVscodeAssociation(settings: string | null): string { + const text = settings?.trim() ? settings : '{}'; + let obj: Record; + try { + obj = JSON.parse(text) as Record; + } catch { + throw new Error( + 'settings.json do VSCode não é JSON válido (comentários?). Adicione à mão:\n' + + ' "files.associations": { "*.psjava": "java" }', + ); + } + const assoc = (obj['files.associations'] ??= {}) as Record; + assoc['*.psjava'] = 'java'; + return JSON.stringify(obj, null, 2) + '\n'; +} + +const INTELLIJ_MAPPING = ''; + +const FRESH_FILETYPES = ` + + + ${INTELLIJ_MAPPING} + + + +`; + +/** + * Garante o mapping `*.psjava → JAVA` no filetypes.xml, preservando o resto. + * Idempotente. Cria um arquivo novo se não existir; insere no `` se já houver. + * Lança em XML de formato inesperado, pra não corromper — aí o usuário adiciona à mão. + */ +export function mergeIntellijMapping(filetypes: string | null): string { + if (!filetypes?.trim()) return FRESH_FILETYPES; + if (intellijHighlightState(filetypes) === 'configured') return filetypes; // idempotente + if (filetypes.includes('')) { + return filetypes.replace('', `\n ${INTELLIJ_MAPPING}`); + } + if (filetypes.includes('')) { + return filetypes.replace( + '', + `\n ${INTELLIJ_MAPPING}\n `, + ); + } + if (filetypes.includes('')) { + return filetypes.replace( + '', + ` \n ${INTELLIJ_MAPPING}\n \n `, + ); + } + throw new Error( + 'filetypes.xml do IntelliJ tem formato inesperado. Adicione à mão dentro de :\n ' + + INTELLIJ_MAPPING, + ); +} diff --git a/src/core/prompt.ts b/src/core/prompt.ts new file mode 100644 index 0000000..e2060fd --- /dev/null +++ b/src/core/prompt.ts @@ -0,0 +1,38 @@ +import { createInterface } from 'readline/promises'; + +export type Ask = (question: string) => Promise; + +/** + * Abre UMA interface de readline e captura cada linha pelo evento `line`, numa fila. + * Assim `ask` sobrevive ao EOF: com input em pipe, as linhas já chegaram na fila antes + * do `close` — sem isso, um `await` entre perguntas deixa o EOF fechar o readline e a + * pergunta seguinte estoura "readline was closed". Lembre de `close()` no fim. + */ +export function createAsker(): { ask: Ask; close: () => void } { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + const lines: string[] = []; + const waiters: ((line: string | null) => void)[] = []; + let closed = false; + + rl.on('line', (line) => { + const next = waiters.shift(); + if (next) next(line); + else lines.push(line); + }); + rl.on('close', () => { + closed = true; + while (waiters.length) waiters.shift()!(null); + }); + + const ask: Ask = async (question) => { + process.stdout.write(question); + const line = lines.length + ? lines.shift()! + : closed + ? null + : await new Promise((resolve) => waiters.push(resolve)); + return (line ?? '').trim(); + }; + + return { ask, close: () => rl.close() }; +} diff --git a/test/cli.test.ts b/test/cli.test.ts index a2426df..6dc251a 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -14,6 +14,12 @@ describe('buildProgram', () => { expect(names).toContain('doctor'); }); + it('expõe highlight install', () => { + const highlight = buildProgram().commands.find((c) => c.name() === 'highlight'); + expect(highlight).toBeDefined(); + expect(highlight!.commands.map((c) => c.name())).toContain('install'); + }); + it('expõe a versão do package.json', () => { expect(buildProgram().version()).toBe(pkg.version); }); diff --git a/test/highlight.test.ts b/test/highlight.test.ts new file mode 100644 index 0000000..f913b73 --- /dev/null +++ b/test/highlight.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect } from 'vitest'; +import { join } from 'path'; +import { + vscodeSettingsPath, + jetbrainsBaseDir, + filetypesPath, + vscodeHighlightState, + intellijHighlightState, + mergeVscodeAssociation, + mergeIntellijMapping, +} from '../src/core/highlight.js'; + +describe('paths de config (Windows)', () => { + const env = { APPDATA: 'C:\\Users\\x\\AppData\\Roaming' } as NodeJS.ProcessEnv; + + it('vscode settings.json mora em Code/User', () => { + expect(vscodeSettingsPath(env)).toContain(join('Code', 'User', 'settings.json')); + }); + + it('jetbrains base é a pasta JetBrains', () => { + expect(jetbrainsBaseDir(env)).toContain('JetBrains'); + }); + + it('filetypes.xml mora em options do produto', () => { + expect(filetypesPath('C:\\dir\\IntelliJIdea2024.1')).toContain(join('options', 'filetypes.xml')); + }); +}); + +describe('vscodeHighlightState', () => { + it('null = IDE não encontrada', () => { + expect(vscodeHighlightState(null)).toBe('ide-not-found'); + }); + it('com a associação = configured', () => { + expect(vscodeHighlightState('{ "files.associations": { "*.psjava": "java" } }')).toBe('configured'); + }); + it('sem a associação = missing', () => { + expect(vscodeHighlightState('{ "editor.fontSize": 14 }')).toBe('missing'); + }); + it('acha a associação mesmo com comentários (JSONC)', () => { + expect( + vscodeHighlightState('{\n // meu editor\n "files.associations": { "*.psjava": "java" }\n}'), + ).toBe('configured'); + }); +}); + +describe('intellijHighlightState', () => { + it('null = produto não encontrado', () => { + expect(intellijHighlightState(null)).toBe('ide-not-found'); + }); + it('com o mapping = configured', () => { + expect(intellijHighlightState('')).toBe('configured'); + }); + it('sem o mapping = missing', () => { + expect(intellijHighlightState('')).toBe('missing'); + }); +}); + +describe('mergeVscodeAssociation', () => { + it('cria a associação quando não há settings (null/vazio)', () => { + const out = mergeVscodeAssociation(null); + expect(vscodeHighlightState(out)).toBe('configured'); + expect(JSON.parse(out)['files.associations']['*.psjava']).toBe('java'); + }); + + it('preserva settings e associações existentes', () => { + const out = mergeVscodeAssociation( + '{ "editor.fontSize": 14, "files.associations": { "*.foo": "bar" } }', + ); + const obj = JSON.parse(out); + expect(obj['editor.fontSize']).toBe(14); + expect(obj['files.associations']['*.foo']).toBe('bar'); + expect(obj['files.associations']['*.psjava']).toBe('java'); + }); + + it('é idempotente (já configurado → continua configurado)', () => { + const once = mergeVscodeAssociation('{}'); + expect(mergeVscodeAssociation(once)).toBe(once); + }); + + it('lança em JSON inválido em vez de corromper o arquivo', () => { + expect(() => mergeVscodeAssociation('{ // comentário\n }')).toThrow(); + }); +}); + +describe('mergeIntellijMapping', () => { + it('cria um filetypes.xml novo quando não existe', () => { + expect(intellijHighlightState(mergeIntellijMapping(null))).toBe('configured'); + }); + + it('insere no existente preservando outros mappings', () => { + const xml = + '' + + ''; + const out = mergeIntellijMapping(xml); + expect(intellijHighlightState(out)).toBe('configured'); + expect(out).toContain('*.foo'); + }); + + it('cria o quando o component não tem um', () => { + const xml = ''; + expect(intellijHighlightState(mergeIntellijMapping(xml))).toBe('configured'); + }); + + it('lida com autofechado', () => { + const xml = ''; + expect(intellijHighlightState(mergeIntellijMapping(xml))).toBe('configured'); + }); + + it('lança em XML sem component nem extensionMap (não corrompe)', () => { + expect(() => mergeIntellijMapping('')).toThrow(); + }); + + it('é idempotente', () => { + const once = mergeIntellijMapping(null); + expect(mergeIntellijMapping(once)).toBe(once); + }); +});