diff --git a/src-web/components/ExportDataDialog.tsx b/src-web/components/ExportDataDialog.tsx index 1219de7d..a5f2ebc1 100644 --- a/src-web/components/ExportDataDialog.tsx +++ b/src-web/components/ExportDataDialog.tsx @@ -4,7 +4,7 @@ import { useCallback, useMemo, useState } from 'react'; import slugify from 'slugify'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useWorkspaces } from '../hooks/useWorkspaces'; -import { count } from '../lib/pluralize'; +import { pluralizeCount } from '../lib/pluralize'; import { invokeCmd } from '../lib/tauri'; import { Button } from './core/Button'; import { Checkbox } from './core/Checkbox'; @@ -128,7 +128,7 @@ function ExportDataDialogContent({ disabled={noneSelected} onClick={() => handleExport()} > - Export {count('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })} + Export {pluralizeCount('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })} diff --git a/src-web/components/GrpcEditor.tsx b/src-web/components/GrpcEditor.tsx index ffa9ffcc..61ab7f50 100644 --- a/src-web/components/GrpcEditor.tsx +++ b/src-web/components/GrpcEditor.tsx @@ -14,7 +14,7 @@ import { useAlert } from '../hooks/useAlert'; import type { ReflectResponseService } from '../hooks/useGrpc'; import { tryFormatJson } from '../lib/formatters'; import type { GrpcRequest } from '@yaakapp-internal/models'; -import { count } from '../lib/pluralize'; +import { pluralizeCount } from '../lib/pluralize'; import { Button } from './core/Button'; import { FormattedError } from './core/FormattedError'; import { InlineCode } from './core/InlineCode'; @@ -164,7 +164,7 @@ export function GrpcEditor({ : reflectionError ? 'Server Error' : protoFiles.length > 0 - ? count('File', protoFiles.length) + ? pluralizeCount('File', protoFiles.length) : services != null && protoFiles.length === 0 ? 'Schema Detected' : 'Select Schema'} diff --git a/src-web/components/GrpcProtoSelection.tsx b/src-web/components/GrpcProtoSelection.tsx index 6c8c7dfa..b14f6686 100644 --- a/src-web/components/GrpcProtoSelection.tsx +++ b/src-web/components/GrpcProtoSelection.tsx @@ -2,7 +2,7 @@ import { open } from '@tauri-apps/plugin-dialog'; import { useGrpc } from '../hooks/useGrpc'; import { useGrpcProtoFiles } from '../hooks/useGrpcProtoFiles'; import { useGrpcRequest } from '../hooks/useGrpcRequest'; -import { count } from '../lib/pluralize'; +import { pluralizeCount } from '../lib/pluralize'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { IconButton } from './core/IconButton'; @@ -76,7 +76,7 @@ export function GrpcProtoSelection({ requestId }: Props) { ); })} - {services?.length > 5 && count('other', services?.length - 5)} + {services?.length > 5 && pluralizeCount('other', services?.length - 5)}

)} diff --git a/src-web/components/RecentConnectionsDropdown.tsx b/src-web/components/RecentConnectionsDropdown.tsx index 956d97f4..fd09c799 100644 --- a/src-web/components/RecentConnectionsDropdown.tsx +++ b/src-web/components/RecentConnectionsDropdown.tsx @@ -2,7 +2,7 @@ import { formatDistanceToNowStrict } from 'date-fns'; import { useDeleteGrpcConnection } from '../hooks/useDeleteGrpcConnection'; import { useDeleteGrpcConnections } from '../hooks/useDeleteGrpcConnections'; import type { GrpcConnection } from '@yaakapp-internal/models'; -import { count } from '../lib/pluralize'; +import { pluralizeCount } from '../lib/pluralize'; import { Dropdown } from './core/Dropdown'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; @@ -34,7 +34,7 @@ export function RecentConnectionsDropdown({ }, { key: 'clear-all', - label: `Clear ${count('Connection', connections.length)}`, + label: `Clear ${pluralizeCount('Connection', connections.length)}`, onSelect: deleteAllConnections.mutate, hidden: connections.length <= 1, disabled: connections.length === 0, diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 1f5cef8e..4cb73050 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -1,6 +1,7 @@ -import { defaultKeymap } from '@codemirror/commands'; -import { forceParsing } from '@codemirror/language'; -import { Compartment, EditorState, type Extension } from '@codemirror/state'; +import { defaultKeymap, historyField } from '@codemirror/commands'; +import { foldState, forceParsing } from '@codemirror/language'; +import type { EditorStateConfig, Extension } from '@codemirror/state'; +import { Compartment, EditorState } from '@codemirror/state'; import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view'; import type { EnvironmentVariable } from '@yaakapp-internal/models'; import type { TemplateFunction } from '@yaakapp-internal/plugin'; @@ -74,6 +75,8 @@ export interface EditorProps { stateKey: string | null; } +const stateFields = { history: historyField, folds: foldState }; + const emptyVariables: EnvironmentVariable[] = []; export const Editor = forwardRef(function Editor( @@ -305,40 +308,42 @@ export const Editor = forwardRef(function E onClickPathParameter, }); + const extensions = [ + languageCompartment.of(langExt), + placeholderCompartment.current.of( + placeholderExt(placeholderElFromText(placeholder ?? '')), + ), + wrapLinesCompartment.current.of(wrapLines ? [EditorView.lineWrapping] : []), + ...getExtensions({ + container, + readOnly, + singleLine, + hideGutter, + stateKey, + onChange: handleChange, + onPaste: handlePaste, + onPasteOverwrite: handlePasteOverwrite, + onFocus: handleFocus, + onBlur: handleBlur, + onKeyDown: handleKeyDown, + }), + ...(extraExtensions ?? []), + ]; + const cachedJsonState = getCachedEditorState(defaultValue ?? '', stateKey); - const state = - cachedJsonState ?? - EditorState.create({ - doc: `${defaultValue ?? ''}`, - extensions: [ - languageCompartment.of(langExt), - placeholderCompartment.current.of( - placeholderExt(placeholderElFromText(placeholder ?? '')), - ), - wrapLinesCompartment.current.of(wrapLines ? [EditorView.lineWrapping] : []), - ...getExtensions({ - container, - readOnly, - singleLine, - hideGutter, - stateKey, - onChange: handleChange, - onPaste: handlePaste, - onPasteOverwrite: handlePasteOverwrite, - onFocus: handleFocus, - onBlur: handleBlur, - onKeyDown: handleKeyDown, - }), - ...(extraExtensions ?? []), - ], - }); + const doc = `${defaultValue ?? ''}`; + const config: EditorStateConfig = { extensions, doc }; + + const state = cachedJsonState + ? EditorState.fromJSON(cachedJsonState, config, stateFields) + : EditorState.create(config); const view = new EditorView({ state, parent: container }); // For large documents, the parser may parse the max number of lines and fail to add // things like fold markers because of it. - // This forces it to parse more but keeps the timeout to the default of 100ms. + // This forces it to parse more but keeps the timeout to the default of 100 ms. forceParsing(view, 9e6, 100); cm.current = { view, languageCompartment }; @@ -544,24 +549,25 @@ const placeholderElFromText = (text: string) => { return el; }; -declare global { - interface Window { - editorStates: Record; - } -} -window.editorStates = window.editorStates ?? {}; - function saveCachedEditorState(stateKey: string | null, state: EditorState | null) { if (!stateKey || state == null) return; - window.editorStates[stateKey] = state; + sessionStorage.setItem(stateKey, JSON.stringify(state.toJSON(stateFields))); } function getCachedEditorState(doc: string, stateKey: string | null) { if (stateKey == null) return; - const state = window.editorStates[stateKey] ?? null; - if (state == null) return null; - if (state.doc.toString() !== doc) return null; + const stateStr = sessionStorage.getItem(stateKey) + if (stateStr == null) return null; - return state; + try { + const state = JSON.parse(stateStr); + if (state.doc !== doc) return null; + + return state; + } catch { + // Nothing + } + + return null; } diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index e0302f42..9df08c2a 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -6,8 +6,8 @@ import { } from '@codemirror/autocomplete'; import { history, historyKeymap, indentWithTab } from '@codemirror/commands'; import { javascript } from '@codemirror/lang-javascript'; -import { markdown } from '@codemirror/lang-markdown'; import { json } from '@codemirror/lang-json'; +import { markdown } from '@codemirror/lang-markdown'; import { xml } from '@codemirror/lang-xml'; import type { LanguageSupport } from '@codemirror/language'; import { @@ -37,7 +37,8 @@ import type { EnvironmentVariable } from '@yaakapp-internal/models'; import type { TemplateFunction } from '@yaakapp-internal/plugin'; import { graphql } from 'cm6-graphql'; import { EditorView } from 'codemirror'; -import type {EditorProps} from "./Editor"; +import { pluralizeCount } from '../../../lib/pluralize'; +import type { EditorProps } from './Editor'; import { pairs } from './pairs/extension'; import { text } from './text/extension'; import { twig } from './twig/extension'; @@ -162,11 +163,15 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => const el = document.createElement('span'); el.onclick = onclick; el.className = 'cm-foldPlaceholder'; - el.innerText = prepared; + el.innerText = prepared || '…'; el.title = 'unfold'; el.ariaLabel = 'folded code'; return el; }, + /** + * Show the number of items when code folded. NOTE: this doesn't get called when restoring + * a previous serialized editor state, which is a bummer + */ preparePlaceholder(state, range) { let count: number | undefined; let startToken = '{'; @@ -191,13 +196,9 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => } if (count !== undefined) { - const label = isArray ? 'item' : 'prop'; - const plural = count === 1 ? '' : 's'; - - return `${count} ${label}${plural}`; + const label = isArray ? 'item' : 'key'; + return pluralizeCount(label, count); } - - return '…'; }, }), EditorState.allowMultipleSelections.of(true), diff --git a/src-web/hooks/useDeleteSendHistory.tsx b/src-web/hooks/useDeleteSendHistory.tsx index 3eb1f419..6ccde5d4 100644 --- a/src-web/hooks/useDeleteSendHistory.tsx +++ b/src-web/hooks/useDeleteSendHistory.tsx @@ -1,5 +1,5 @@ import { useSetAtom } from 'jotai/index'; -import { count } from '../lib/pluralize'; +import { pluralizeCount } from '../lib/pluralize'; import { invokeCmd } from '../lib/tauri'; import { getActiveWorkspaceId } from './useActiveWorkspace'; import { useAlert } from './useAlert'; @@ -15,8 +15,8 @@ export function useDeleteSendHistory() { const httpResponses = useHttpResponses(); const grpcConnections = useGrpcConnections(); const labels = [ - httpResponses.length > 0 ? count('Http Response', httpResponses.length) : null, - grpcConnections.length > 0 ? count('Grpc Connection', grpcConnections.length) : null, + httpResponses.length > 0 ? pluralizeCount('Http Response', httpResponses.length) : null, + grpcConnections.length > 0 ? pluralizeCount('Grpc Connection', grpcConnections.length) : null, ].filter((l) => l != null); return useFastMutation({ diff --git a/src-web/hooks/useImportData.tsx b/src-web/hooks/useImportData.tsx index 3abf0161..e8561339 100644 --- a/src-web/hooks/useImportData.tsx +++ b/src-web/hooks/useImportData.tsx @@ -10,7 +10,7 @@ import { Button } from '../components/core/Button'; import { FormattedError } from '../components/core/FormattedError'; import { VStack } from '../components/core/Stacks'; import { ImportDataDialog } from '../components/ImportDataDialog'; -import { count } from '../lib/pluralize'; +import { pluralizeCount } from '../lib/pluralize'; import { invokeCmd } from '../lib/tauri'; import { getActiveWorkspace } from './useActiveWorkspace'; import { useAlert } from './useAlert'; @@ -47,11 +47,11 @@ export function useImportData() { return (
    -
  • {count('Workspace', workspaces.length)}
  • -
  • {count('Environment', environments.length)}
  • -
  • {count('Folder', folders.length)}
  • -
  • {count('HTTP Request', httpRequests.length)}
  • -
  • {count('GRPC Request', grpcRequests.length)}
  • +
  • {pluralizeCount('Workspace', workspaces.length)}
  • +
  • {pluralizeCount('Environment', environments.length)}
  • +
  • {pluralizeCount('Folder', folders.length)}
  • +
  • {pluralizeCount('HTTP Request', httpRequests.length)}
  • +
  • {pluralizeCount('GRPC Request', grpcRequests.length)}