From 24d57acb41891fd5f670be713a15484297c77755 Mon Sep 17 00:00:00 2001 From: swk777 Date: Thu, 5 Sep 2024 15:04:40 +0800 Subject: [PATCH] use prettier command to format code --- packages/api/server/ws.mts | 59 ++++++++++++++----- packages/api/session.mts | 31 ++++++++++ packages/api/srcbook/config.mts | 5 +- packages/shared/src/schemas/websockets.ts | 5 ++ packages/shared/src/types/websockets.ts | 2 + packages/web/package.json | 2 +- packages/web/src/clients/websocket/index.ts | 2 + packages/web/src/components/cells/code.tsx | 53 ++++++++++++++++- .../components/keyboard-shortcuts-dialog.tsx | 1 + pnpm-lock.yaml | 10 ++-- 10 files changed, 146 insertions(+), 24 deletions(-) diff --git a/packages/api/server/ws.mts b/packages/api/server/ws.mts index 26b64015..a49aa2dd 100644 --- a/packages/api/server/ws.mts +++ b/packages/api/server/ws.mts @@ -11,6 +11,7 @@ import { removeCell, updateCodeCellFilename, addCell, + formatAndUpdateCodeCell, } from '../session.mjs'; import { getSecrets } from '../config.mjs'; import type { SessionType } from '../types.mjs'; @@ -25,6 +26,7 @@ import type { DepsValidatePayloadType, CellStopPayloadType, CellUpdatePayloadType, + CellFormatPayloadType, TsServerStartPayloadType, TsServerStopPayloadType, CellDeletePayloadType, @@ -41,6 +43,7 @@ import { CellUpdatedPayloadSchema, CellRenamePayloadSchema, CellDeletePayloadSchema, + CellFormatPayloadSchema, CellExecPayloadSchema, CellStopPayloadSchema, AiGenerateCellPayloadSchema, @@ -413,6 +416,31 @@ async function cellFixDiagnostics(payload: AiFixDiagnosticsPayloadType) { }); } +async function cellFormat(payload: CellFormatPayloadType) { + const session = await findSession(payload.sessionId); + console.log('in'); + if (!session) { + throw new Error(`No session exists for session '${payload.sessionId}'`); + } + const cellBeforeUpdate = findCell(session, payload.cellId); + + if (!cellBeforeUpdate || cellBeforeUpdate.type !== 'code') { + throw new Error( + `No cell exists or not a code cell for session '${payload.sessionId}' and cell '${payload.cellId}'`, + ); + } + const result = await formatAndUpdateCodeCell(session, cellBeforeUpdate); + if (!result.success) { + return sendCellUpdateError(session, payload.cellId, result.errors); + } + + const cell = result.cell as CodeCellType; + + wss.broadcast(`session:${session.id}`, 'cell:updated', { cell }); + + refreshCodeCellDiagnostics(session, cell); +} + async function cellUpdate(payload: CellUpdatePayloadType) { const session = await findSession(payload.sessionId); @@ -427,7 +455,6 @@ async function cellUpdate(payload: CellUpdatePayloadType) { `No cell exists for session '${payload.sessionId}' and cell '${payload.cellId}'`, ); } - const result = await updateCell(session, cellBeforeUpdate, payload.updates); if (!result.success) { @@ -436,19 +463,7 @@ async function cellUpdate(payload: CellUpdatePayloadType) { const cell = result.cell as CodeCellType; - if (session.language === 'typescript' && cell.type === 'code' && tsservers.has(session.id)) { - const tsserver = tsservers.get(session.id); - - // This isn't intended for renaming, so the filenames - // and their resulting paths are expected to be the same - reopenFileInTsServer(tsserver, session, { - openFilename: cell.filename, - closeFilename: cell.filename, - source: cell.source, - }); - - requestAllDiagnostics(tsserver, session); - } + refreshCodeCellDiagnostics(session, cell); } async function cellRename(payload: CellRenamePayloadType) { @@ -682,6 +697,21 @@ async function tsconfigUpdate(payload: TsConfigUpdatePayloadType) { }); } +function refreshCodeCellDiagnostics(session: SessionType, cell: CodeCellType) { + if (session.language === 'typescript' && cell.type === 'code' && tsservers.has(session.id)) { + const tsserver = tsservers.get(session.id); + + // This isn't intended for renaming, so the filenames + // and their resulting paths are expected to be the same + reopenFileInTsServer(tsserver, session, { + openFilename: cell.filename, + closeFilename: cell.filename, + source: cell.source, + }); + + requestAllDiagnostics(tsserver, session); + } +} wss .channel('session:*') .incoming('cell:exec', CellExecPayloadSchema, cellExec) @@ -690,6 +720,7 @@ wss .incoming('cell:update', CellUpdatePayloadSchema, cellUpdate) .incoming('cell:rename', CellRenamePayloadSchema, cellRename) .incoming('cell:delete', CellDeletePayloadSchema, cellDelete) + .incoming('cell:format', CellFormatPayloadSchema, cellFormat) .incoming('ai:generate', AiGenerateCellPayloadSchema, cellGenerate) .incoming('ai:fix_diagnostics', AiFixDiagnosticsPayloadSchema, cellFixDiagnostics) .incoming('deps:install', DepsInstallPayloadSchema, depsInstall) diff --git a/packages/api/session.mts b/packages/api/session.mts index 8abd6fb0..8aab5d3c 100644 --- a/packages/api/session.mts +++ b/packages/api/session.mts @@ -29,6 +29,7 @@ import { import { fileExists } from './fs-utils.mjs'; import { validFilename } from '@srcbook/shared'; import { pathToCodeFile } from './srcbook/path.mjs'; +import { exec } from 'node:child_process'; const sessions: Record = {}; @@ -293,6 +294,36 @@ export function updateCell(session: SessionType, cell: CellType, updates: CellUp } } +export async function formatCode(filePath: string) { + try { + const command = `npx prettier --no-error-on-unmatched-pattern ${filePath}`; + + return new Promise((resolve, reject) => { + exec(command, async (error, stdout) => { + if (error) { + console.error(`exec error: ${error}`); + reject(error); + return; + } + resolve(stdout); + }); + }); + } catch (error) { + console.error('Formatting error:', error); + throw error; + } +} +export async function formatAndUpdateCodeCell(session: SessionType, cell: CodeCellType) { + try { + const formattedCode = await formatCode(pathToCodeFile(session.dir, cell.filename)); + return updateCodeCell(session, cell, { source: formattedCode } as { source: string }); + } catch (error) { + return Promise.resolve({ + success: false, + errors: [{ message: 'An error occurred formatting the code.', attribute: 'formatting' }], + } as UpdateResultType); + } +} export function sessionToResponse(session: SessionType) { const result: Pick = { id: session.id, diff --git a/packages/api/srcbook/config.mts b/packages/api/srcbook/config.mts index 8873bfde..9e129e84 100644 --- a/packages/api/srcbook/config.mts +++ b/packages/api/srcbook/config.mts @@ -1,7 +1,9 @@ export function buildJSPackageJson() { return { type: 'module', - dependencies: {}, + dependencies: { + prettier: 'latest', + }, }; } @@ -12,6 +14,7 @@ export function buildTSPackageJson() { tsx: 'latest', typescript: 'latest', '@types/node': 'latest', + prettier: 'latest', }, }; } diff --git a/packages/shared/src/schemas/websockets.ts b/packages/shared/src/schemas/websockets.ts index a9728379..91126987 100644 --- a/packages/shared/src/schemas/websockets.ts +++ b/packages/shared/src/schemas/websockets.ts @@ -31,6 +31,11 @@ export const CellUpdatePayloadSchema = z.object({ updates: CellUpdateAttrsSchema, }); +export const CellFormatPayloadSchema = z.object({ + sessionId: z.string(), + cellId: z.string(), +}); + export const AiGenerateCellPayloadSchema = z.object({ sessionId: z.string(), cellId: z.string(), diff --git a/packages/shared/src/types/websockets.ts b/packages/shared/src/types/websockets.ts index bdb3b7f6..e47709d5 100644 --- a/packages/shared/src/types/websockets.ts +++ b/packages/shared/src/types/websockets.ts @@ -6,6 +6,7 @@ import { CellCreatePayloadSchema, CellUpdatePayloadSchema, CellUpdatedPayloadSchema, + CellFormatPayloadSchema, CellRenamePayloadSchema, CellDeletePayloadSchema, AiGenerateCellPayloadSchema, @@ -28,6 +29,7 @@ export type CellExecPayloadType = z.infer; export type CellStopPayloadType = z.infer; export type CellCreatePayloadType = z.infer; export type CellUpdatePayloadType = z.infer; +export type CellFormatPayloadType = z.infer; export type CellUpdatedPayloadType = z.infer; export type CellRenamePayloadType = z.infer; export type CellDeletePayloadType = z.infer; diff --git a/packages/web/package.json b/packages/web/package.json index df4e8582..b602ccf3 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -37,7 +37,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.0", "codemirror": "^6.0.1", - "lucide-react": "^0.378.0", + "lucide-react": "^0.380.0", "marked": "catalog:", "marked-react": "^2.0.0", "react": "^18.2.0", diff --git a/packages/web/src/clients/websocket/index.ts b/packages/web/src/clients/websocket/index.ts index 657aa1e4..37c78808 100644 --- a/packages/web/src/clients/websocket/index.ts +++ b/packages/web/src/clients/websocket/index.ts @@ -4,6 +4,7 @@ import { AiGenerateCellPayloadSchema, AiGeneratedCellPayloadSchema, CellUpdatedPayloadSchema, + CellFormatPayloadSchema, DepsValidateResponsePayloadSchema, CellExecPayloadSchema, CellStopPayloadSchema, @@ -47,6 +48,7 @@ const OutgoingSessionEvents = { 'cell:update': CellUpdatePayloadSchema, 'cell:rename': CellRenamePayloadSchema, 'cell:delete': CellDeletePayloadSchema, + 'cell:format': CellFormatPayloadSchema, 'ai:generate': AiGenerateCellPayloadSchema, 'ai:fix_diagnostics': AiFixDiagnosticsPayloadSchema, 'deps:install': DepsInstallPayloadSchema, diff --git a/packages/web/src/components/cells/code.tsx b/packages/web/src/components/cells/code.tsx index 6293b7c7..92a9e770 100644 --- a/packages/web/src/components/cells/code.tsx +++ b/packages/web/src/components/cells/code.tsx @@ -17,6 +17,7 @@ import { LoaderCircle, Maximize, Minimize, + PaintbrushVertical, } from 'lucide-react'; import TextareaAutosize from 'react-textarea-autosize'; import AiGenerateTipsDialog from '@/components/ai-generate-tips-dialog'; @@ -43,6 +44,7 @@ import { EditorView } from 'codemirror'; import { EditorState } from '@codemirror/state'; import { unifiedMergeView } from '@codemirror/merge'; import { type Diagnostic, linter } from '@codemirror/lint'; +import { toast } from 'sonner'; const DEBOUNCE_DELAY = 500; type CellModeType = 'off' | 'generating' | 'reviewing' | 'prompting' | 'fixing'; @@ -62,9 +64,7 @@ export default function CodeCell(props: { const [prompt, setPrompt] = useState(''); const [newSource, setNewSource] = useState(''); const [fullscreen, setFullscreen] = useState(false); - const { aiEnabled } = useSettings(); - useHotkeys( 'mod+enter', () => { @@ -97,6 +97,7 @@ export default function CodeCell(props: { useEffect(() => { function callback(payload: CellErrorPayloadType) { + console.log(payload); if (payload.cellId !== cell.id) { return; } @@ -106,6 +107,11 @@ export default function CodeCell(props: { if (filenameError) { setFilenameError(filenameError.message); } + + const formattingError = payload.errors.find((e) => e.attribute === 'formatting'); + if (formattingError) { + toast.error(formattingError.message); + } } channel.on('cell:error', callback); @@ -186,6 +192,12 @@ export default function CodeCell(props: { setCellMode('off'); } + function formatCell() { + channel.push('cell:format', { + sessionId: session.id, + cellId: cell.id, + }); + } return (
@@ -220,6 +232,7 @@ export default function CodeCell(props: { setShowStdio={setShowStdio} onAccept={onAcceptDiff} onRevert={onRevertDiff} + formatCell={formatCell} /> {cellMode === 'reviewing' ? ( @@ -231,6 +244,7 @@ export default function CodeCell(props: { @@ -285,6 +299,7 @@ export default function CodeCell(props: { setShowStdio={setShowStdio} onAccept={onAcceptDiff} onRevert={onRevertDiff} + formatCell={formatCell} /> {cellMode === 'reviewing' ? ( @@ -295,6 +310,7 @@ export default function CodeCell(props: { @@ -334,6 +350,7 @@ function Header(props: { stopCell: () => void; onAccept: () => void; onRevert: () => void; + formatCell: () => void; }) { const { cell, @@ -351,6 +368,7 @@ function Header(props: { prompt, setPrompt, stopCell, + formatCell, } = props; const { aiEnabled } = useSettings(); @@ -396,6 +414,22 @@ function Header(props: { )} >
+ + + + + + Format + + @@ -626,11 +660,13 @@ function tsLinter( function CodeEditor({ cell, runCell, + formatCell, updateCellOnServer, readOnly, }: { cell: CodeCellType; runCell: () => void; + formatCell: () => void; updateCellOnServer: (cell: CodeCellType, attrs: CodeCellUpdateAttrsType) => void; readOnly: boolean; }) { @@ -652,7 +688,18 @@ function CodeEditor({ javascript({ typescript: true }), // wordHoverExtension, tsLinter(cell, getTsServerDiagnostics, getTsServerSuggestions), - Prec.highest(keymap.of([{ key: 'Mod-Enter', run: evaluateModEnter }])), + Prec.highest( + keymap.of([ + { key: 'Mod-Enter', run: evaluateModEnter }, + { + key: 'Mod-Shift-f', + run: () => { + formatCell(); + return true; + }, + }, + ]), + ), ]; if (readOnly) { extensions = extensions.concat([EditorView.editable.of(false), EditorState.readOnly.of(true)]); diff --git a/packages/web/src/components/keyboard-shortcuts-dialog.tsx b/packages/web/src/components/keyboard-shortcuts-dialog.tsx index b24794af..fee83917 100644 --- a/packages/web/src/components/keyboard-shortcuts-dialog.tsx +++ b/packages/web/src/components/keyboard-shortcuts-dialog.tsx @@ -44,6 +44,7 @@ export default function KeyboardShortcutsDialog({ +
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a7f4599..9550f7bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,8 +206,8 @@ importers: specifier: ^6.0.1 version: 6.0.1(@lezer/common@1.2.1) lucide-react: - specifier: ^0.378.0 - version: 0.378.0(react@18.3.1) + specifier: ^0.380.0 + version: 0.380.0(react@18.3.1) marked: specifier: 'catalog:' version: 12.0.2 @@ -3268,8 +3268,8 @@ packages: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} - lucide-react@0.378.0: - resolution: {integrity: sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g==} + lucide-react@0.380.0: + resolution: {integrity: sha512-PkxF3wEnoqq2v2LjMXO98+3pOowXTgV/BRm/2VeU9THAAbRQYJMjO77vLYEfWEBtC1Y0HR5H6dNa9cdcoRYSdA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 @@ -7220,7 +7220,7 @@ snapshots: lru-cache@10.2.2: {} - lucide-react@0.378.0(react@18.3.1): + lucide-react@0.380.0(react@18.3.1): dependencies: react: 18.3.1