Skip to content

Commit fa453d4

Browse files
feat(desktop): add JSONC-aware body editor with syntax highlighting (#90)
1 parent db6027a commit fa453d4

File tree

8 files changed

+665
-50
lines changed

8 files changed

+665
-50
lines changed

bun.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/desktop/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@
1717
"license": "MIT",
1818
"dependencies": {
1919
"@t-req/sdk": "workspace:*",
20+
"@codemirror/commands": "^6.10.0",
21+
"@codemirror/language": "^6.10.0",
22+
"@codemirror/lang-json": "^6.0.2",
23+
"@codemirror/state": "^6.5.2",
24+
"@codemirror/view": "^6.38.6",
25+
"@lezer/highlight": "^1.2.3",
2026
"@tauri-apps/api": "^2",
2127
"@tauri-apps/plugin-dialog": "^2",
2228
"@tauri-apps/plugin-opener": "^2",
2329
"@tauri-apps/plugin-shell": "^2",
30+
"codemirror": "^6.0.2",
2431
"solid-js": "^1.9.3"
2532
},
2633
"devDependencies": {

packages/desktop/src/features/explorer/components/ExplorerScreen.tsx

Lines changed: 115 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '../create-request';
2121
import { FALLBACK_REQUEST_METHOD, FALLBACK_REQUEST_URL } from '../request-line';
2222
import { useExplorerStore } from '../use-explorer-store';
23+
import { formatJsonBodyText, validateJsonBodyText } from '../utils/json-body';
2324
import { buildCreateFilePath, toCreateHttpPath } from '../utils/mutations';
2425
import { parentDirectory } from '../utils/path';
2526
import {
@@ -282,7 +283,16 @@ export default function ExplorerScreen() {
282283
const request = selectedRequest();
283284
const sourceUrl = requestSourceUrl();
284285
const content = explorer.fileDraftContent();
285-
if (!request || !sourceUrl || content === undefined) {
286+
if (!request) {
287+
setDetailsSaveError('Select a request before saving request details.');
288+
return;
289+
}
290+
if (!sourceUrl) {
291+
setDetailsSaveError('Unable to resolve the request URL for this request.');
292+
return;
293+
}
294+
if (content === undefined) {
295+
setDetailsSaveError('Request file content is still loading. Try saving again.');
286296
return;
287297
}
288298

@@ -350,10 +360,26 @@ export default function ExplorerScreen() {
350360

351361
return sourceUrl;
352362
});
353-
const requestParams = createMemo(() => draftParams());
354-
const requestHeaders = createMemo(() => draftHeaders());
355-
const requestBodySummary = createMemo(() => requestSourceBody());
356-
const requestDiagnostics = createMemo(() => requestSourceDiagnostics());
363+
const inlineJsonBodyText = createMemo(() => {
364+
const body = requestSourceBody();
365+
if (body.kind !== 'inline' || !body.isJsonLike) {
366+
return undefined;
367+
}
368+
369+
if (isDetailsDirty()) {
370+
return draftBody();
371+
}
372+
373+
return body.text ?? '';
374+
});
375+
const bodyValidationError = createMemo(() => {
376+
const text = inlineJsonBodyText();
377+
if (text === undefined) {
378+
return undefined;
379+
}
380+
return validateJsonBodyText(text);
381+
});
382+
const hasSelectedRequest = createMemo(() => Boolean(selectedRequest()));
357383
const fileDiagnostics = createMemo(() => parsedRequestFile()?.diagnostics ?? []);
358384

359385
const requestDetailsError = createMemo(() => {
@@ -365,7 +391,20 @@ export default function ExplorerScreen() {
365391
const isRequestDetailsLoading = createMemo(
366392
() => Boolean(parseSource()) && parsedRequestFile.loading
367393
);
368-
const isUnsupportedProtocol = createMemo(() => !isHttpProtocol(selectedRequest()?.protocol));
394+
const isUnsupportedProtocol = createMemo(() => {
395+
const request = selectedRequest();
396+
if (!request) {
397+
return false;
398+
}
399+
return !isHttpProtocol(request.protocol);
400+
});
401+
const unsupportedProtocolLabel = createMemo(() => {
402+
const request = selectedRequest();
403+
if (!request || !isUnsupportedProtocol()) {
404+
return undefined;
405+
}
406+
return request.protocol?.toUpperCase() ?? 'THIS';
407+
});
369408
const isBusy = createMemo(() => explorer.isMutating());
370409
const sendDisabled = createMemo(() => {
371410
if (!selectedPath() || !selectedRequest() || !server.client()) {
@@ -374,6 +413,9 @@ export default function ExplorerScreen() {
374413
if (isUnsupportedProtocol()) {
375414
return true;
376415
}
416+
if (bodyValidationError()) {
417+
return true;
418+
}
377419
return isBusy() || isFileLoading() || isRequestsLoading() || isSavingFile() || isSending();
378420
});
379421
const explorerGridStyle = createMemo<Record<string, string>>(() => ({
@@ -512,6 +554,54 @@ export default function ExplorerScreen() {
512554
}
513555
};
514556

557+
const prettifyDraftBody = () => {
558+
const body = requestSourceBody();
559+
if (body.kind !== 'inline' || !body.isJsonLike) {
560+
return;
561+
}
562+
563+
const result = formatJsonBodyText(draftBody(), 'prettify');
564+
if (!result.ok) {
565+
return;
566+
}
567+
568+
setDraftBody(result.text);
569+
markDetailsDirty();
570+
};
571+
572+
const minifyDraftBody = () => {
573+
const body = requestSourceBody();
574+
if (body.kind !== 'inline' || !body.isJsonLike) {
575+
return;
576+
}
577+
578+
const result = formatJsonBodyText(draftBody(), 'minify');
579+
if (!result.ok) {
580+
return;
581+
}
582+
583+
setDraftBody(result.text);
584+
markDetailsDirty();
585+
};
586+
587+
const copyDraftBody = async () => {
588+
if (!selectedRequest()) {
589+
return;
590+
}
591+
592+
try {
593+
await navigator.clipboard.writeText(draftBody());
594+
} catch {
595+
// Ignore clipboard failures and keep editing flow uninterrupted.
596+
}
597+
};
598+
599+
const refreshExplorer = () => void explorer.refresh();
600+
const submitCreateDialogRequest = () => void submitCreateDialog();
601+
const sendSelectedRequestAction = () => void sendSelectedRequest();
602+
const copyDraftBodyAction = () => void copyDraftBody();
603+
const saveRequestDetailsDraftAction = () => void saveRequestDetailsDraft();
604+
515605
return (
516606
<main
517607
class="flex-1 min-h-0 overflow-hidden grid grid-cols-[var(--explorer-grid-cols)] gap-0 px-2 pt-2 max-[960px]:grid-cols-1 max-[960px]:grid-rows-[var(--explorer-grid-rows-mobile)]"
@@ -524,7 +614,7 @@ export default function ExplorerScreen() {
524614
>
525615
<ExplorerToolbar
526616
onCreate={openCreateDialog}
527-
onRefresh={() => void explorer.refresh()}
617+
onRefresh={refreshExplorer}
528618
isRefreshing={explorer.isLoading()}
529619
isMutating={isBusy()}
530620
workspaceRoot={explorer.workspaceRoot()}
@@ -601,7 +691,7 @@ export default function ExplorerScreen() {
601691
onClose={closeCreateDialog}
602692
onNameChange={(value) => setCreateDialog('name', value)}
603693
onKindChange={(kind) => setCreateDialog('kind', kind)}
604-
onSubmit={() => void submitCreateDialog()}
694+
onSubmit={submitCreateDialogRequest}
605695
/>
606696

607697
<section
@@ -679,12 +769,12 @@ export default function ExplorerScreen() {
679769
</div>
680770
</Show>
681771

682-
<Show when={isUnsupportedProtocol() && selectedRequest()}>
683-
<div class="alert mx-3 mt-3 border border-base-300 bg-base-200/70 text-base-content">
684-
<span class="text-sm">
685-
{selectedRequest()?.protocol?.toUpperCase()} execution wiring is coming next.
686-
</span>
687-
</div>
772+
<Show when={unsupportedProtocolLabel()}>
773+
{(protocol) => (
774+
<div class="alert mx-3 mt-3 border border-base-300 bg-base-200/70 text-base-content">
775+
<span class="text-sm">{protocol()} execution wiring is coming next.</span>
776+
</div>
777+
)}
688778
</Show>
689779

690780
<RequestUrlBar
@@ -693,7 +783,7 @@ export default function ExplorerScreen() {
693783
requestOptions={requestOptions()}
694784
selectedRequestIndex={selectedRequestIndex()}
695785
onRequestIndexChange={handleRequestIndexChange}
696-
onSend={() => void sendSelectedRequest()}
786+
onSend={sendSelectedRequestAction}
697787
disabled={isBusy() || isFileLoading() || isRequestsLoading() || isSavingFile()}
698788
sendDisabled={sendDisabled()}
699789
isSending={isSending()}
@@ -704,12 +794,13 @@ export default function ExplorerScreen() {
704794
style={requestPanelsStyle()}
705795
>
706796
<RequestDetailsPanel
707-
hasRequest={Boolean(selectedRequest())}
708-
params={requestParams()}
709-
headers={requestHeaders()}
710-
bodySummary={requestBodySummary()}
797+
hasRequest={hasSelectedRequest()}
798+
params={draftParams()}
799+
headers={draftHeaders()}
800+
bodySummary={requestSourceBody()}
711801
bodyDraft={draftBody()}
712-
diagnostics={requestDiagnostics()}
802+
bodyValidationError={bodyValidationError()}
803+
diagnostics={requestSourceDiagnostics()}
713804
fileDiagnostics={fileDiagnostics()}
714805
isLoading={isRequestDetailsLoading()}
715806
error={requestDetailsError()}
@@ -723,7 +814,10 @@ export default function ExplorerScreen() {
723814
onAddHeader={addDraftHeader}
724815
onRemoveHeader={removeDraftHeader}
725816
onBodyChange={handleDraftBodyChange}
726-
onSave={() => void saveRequestDetailsDraft()}
817+
onBodyPrettify={prettifyDraftBody}
818+
onBodyMinify={minifyDraftBody}
819+
onBodyCopy={copyDraftBodyAction}
820+
onSave={saveRequestDetailsDraftAction}
727821
onDiscard={discardRequestDetailsDraft}
728822
/>
729823
<Show

0 commit comments

Comments
 (0)