remove codemirror dep and restructure a bit

This commit is contained in:
Gregory Schier
2025-05-15 09:28:14 -07:00
parent 21e2a67c1e
commit d962d7f94b
25 changed files with 278 additions and 2337 deletions

View File

@@ -1,6 +1,6 @@
import type { EditorView } from '@codemirror/view';
import type { HttpRequest } from '@yaakapp-internal/models';
import { updateSchema } from 'cm6-graphql';
import type { EditorView } from 'codemirror';
import { formatSdl } from 'format-graphql';
import { useEffect, useMemo, useRef } from 'react';
@@ -60,7 +60,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
// Refetch the schema when the URL changes
useEffect(() => {
if (editorViewRef.current === null) return;
if (editorViewRef.current == null) return;
updateSchema(editorViewRef.current, schema ?? undefined);
}, [schema]);

View File

@@ -1,8 +1,8 @@
import { jsonLanguage } from '@codemirror/lang-json';
import { linter } from '@codemirror/lint';
import type { EditorView } from '@codemirror/view';
import type { GrpcRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import {
handleRefresh,
jsonCompletion,

View File

@@ -4,7 +4,7 @@ import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import React, { lazy, useCallback, useMemo, useState } from 'react';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
@@ -44,13 +44,18 @@ import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { FormMultipartEditor } from './FormMultipartEditor';
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
import { GraphQLEditor } from './GraphQLEditor';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor';
const GraphQLEditor = lazy(() =>
import('./GraphQLEditor').then((m) => ({ default: m.GraphQLEditor })),
);
const MarkdownEditor = lazy(() =>
import('./MarkdownEditor').then((m) => ({ default: m.MarkdownEditor })),
);
interface Props {
style: CSSProperties;
fullHeight: boolean;

View File

@@ -1,7 +1,7 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react';
import React, { useCallback, useMemo } from 'react';
import React, { lazy , useCallback, useMemo } from 'react';
import { useLocalStorage } from 'react-use';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
@@ -27,7 +27,10 @@ import { CsvViewer } from './responseViewers/CsvViewer';
import { EventStreamViewer } from './responseViewers/EventStreamViewer';
import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { PdfViewer } from './responseViewers/PdfViewer';
const PdfViewer = lazy(() =>
import('./responseViewers/PdfViewer').then((m) => ({ default: m.PdfViewer })),
);
import { SvgViewer } from './responseViewers/SvgViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
import { ErrorBoundary } from './ErrorBoundary';

View File

@@ -6,7 +6,6 @@ import { appInfo } from '../hooks/useAppInfo';
import { useLicenseConfirmation } from '../hooks/useLicenseConfirmation';
import { BadgeButton } from './core/BadgeButton';
import type { ButtonProps } from './core/Button';
import { SettingsTab } from './Settings/SettingsTab';
const details: Record<
LicenseCheckStatus['type'],
@@ -28,7 +27,7 @@ export function LicenseBadge() {
if (check.error) {
return (
<BadgeButton color="danger" onClick={() => openSettings.mutate(SettingsTab.License)}>
<BadgeButton color="danger" onClick={() => openSettings.mutate('license')}>
License Error
</BadgeButton>
);
@@ -64,7 +63,7 @@ export function LicenseBadge() {
hasDismissedTrial: true,
}));
}
openSettings.mutate(SettingsTab.License);
openSettings.mutate('license');
}}
>
{detail.label}

View File

@@ -13,24 +13,23 @@ import { SettingsGeneral } from './SettingsGeneral';
import { SettingsLicense } from './SettingsLicense';
import { SettingsPlugins } from './SettingsPlugins';
import { SettingsProxy } from './SettingsProxy';
import { SettingsTab } from './SettingsTab';
interface Props {
hide?: () => void;
}
const tabs = [
SettingsTab.General,
SettingsTab.Appearance,
SettingsTab.Proxy,
SettingsTab.Plugins,
SettingsTab.License,
];
const TAB_GENERAL = 'general';
const TAB_APPEARANCE = 'appearance';
const TAB_PROXY = 'proxy';
const TAB_PLUGINS = 'plugins';
const TAB_LICENSE = 'license';
const tabs = [TAB_GENERAL, TAB_APPEARANCE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE] as const;
export type SettingsTab = (typeof tabs)[number];
export default function Settings({ hide }: Props) {
const osInfo = useOsInfo();
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
const [tab, setTab] = useState<string>(tabFromQuery ?? SettingsTab.General);
const [tab, setTab] = useState<string | undefined>(tabFromQuery);
// Close settings window on escape
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
@@ -74,19 +73,19 @@ export default function Settings({ hide }: Props) {
onChangeValue={setTab}
tabs={tabs.map((value) => ({ value, label: capitalize(value) }))}
>
<TabContent value={SettingsTab.General} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
<SettingsGeneral />
</TabContent>
<TabContent value={SettingsTab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={TAB_APPEARANCE} className="pt-3 overflow-y-auto h-full px-4">
<SettingsAppearance />
</TabContent>
<TabContent value={SettingsTab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={TAB_PLUGINS} className="pt-3 overflow-y-auto h-full px-4">
<SettingsPlugins />
</TabContent>
<TabContent value={SettingsTab.Proxy} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={TAB_PROXY} className="pt-3 overflow-y-auto h-full px-4">
<SettingsProxy />
</TabContent>
<TabContent value={SettingsTab.License} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={TAB_LICENSE} className="pt-3 overflow-y-auto h-full px-4">
<SettingsLicense />
</TabContent>
</Tabs>

View File

@@ -1,8 +0,0 @@
export enum SettingsTab {
General = 'general',
Proxy = 'proxy',
Appearance = 'appearance',
Plugins = 'plugins',
License = 'license',
}

View File

@@ -13,7 +13,6 @@ import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
import { SettingsTab } from './Settings/SettingsTab';
export function SettingsDropdown() {
const importData = useImportData();
@@ -64,7 +63,7 @@ export function SettingsDropdown() {
color: 'success',
hidden: check.data == null || check.data.type === 'commercial_use',
leftSlot: <Icon icon="circle_dollar_sign" />,
onSelect: () => openSettings.mutate(SettingsTab.License),
onSelect: () => openSettings.mutate('license'),
},
{
label: 'Check for Updates',

View File

@@ -1,6 +1,6 @@
import type { EditorView } from '@codemirror/view';
import type { HttpRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import type { FormEvent, ReactNode } from 'react';
import { memo, useRef, useState } from 'react';
import { useHotKey } from '../hooks/useHotKey';
@@ -8,7 +8,7 @@ import type { IconProps } from './core/Icon';
import { IconButton } from './core/IconButton';
import type { InputProps } from './core/Input';
import { Input } from './core/Input';
import {HStack} from "./core/Stacks";
import { HStack } from './core/Stacks';
import { RequestMethodDropdown } from './RequestMethodDropdown';
type Props = Pick<HttpRequest, 'url'> & {

View File

@@ -194,12 +194,18 @@ function WorkspaceBody() {
if (activeWorkspace == null) {
return (
<div className="m-auto">
<m.div
className="m-auto"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
// Delay the entering because the workspaces might load after a slight delay
transition={{ delay: 0.5 }}
>
<Banner color="warning" className="max-w-[30rem]">
The active workspace was not found. Select a workspace from the header menu or report this
bug to <FeedbackLink />
</Banner>
</div>
</m.div>
);
}

View File

@@ -2,16 +2,15 @@ import { defaultKeymap, historyField, indentWithTab } 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 { EditorView, keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import type { EditorKeymap, EnvironmentVariable } from '@yaakapp-internal/models';
import { settingsAtom } from '@yaakapp-internal/models';
import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins';
import { parseTemplate } from '@yaakapp-internal/templates';
import classNames from 'classnames';
import { EditorView } from 'codemirror';
import { useAtomValue } from 'jotai';
import { md5 } from 'js-md5';
import type { MutableRefObject, ReactNode } from 'react';
@@ -48,6 +47,8 @@ import {
import type { GenericCompletionConfig } from './genericCompletion';
import { singleLineExtensions } from './singleLine';
const { vim } = await import('@replit/codemirror-vim');
// VSCode's Tab actions mess with the single-line editor tab actions, so remove it.
const vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== 'Tab');
@@ -365,7 +366,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
useEffect(() => {
if (cm.current === null) return;
const { view, languageCompartment } = cm.current;
const ext = getLanguageExtension({
getLanguageExtension({
useTemplating,
language,
environmentVariables,
@@ -374,8 +375,9 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
onClickVariable,
onClickMissingVariable,
onClickPathParameter,
}).then((ext) => {
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
});
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
}, [
language,
autocomplete,
@@ -399,7 +401,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
try {
const languageCompartment = new Compartment();
const langExt = getLanguageExtension({
getLanguageExtension({
useTemplating,
language,
completionOptions,
@@ -408,57 +410,57 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
onClickVariable,
onClickMissingVariable,
onClickPathParameter,
}).then((langExt) => {
const extensions = [
languageCompartment.of(langExt),
placeholderCompartment.current.of(placeholderExt(placeholderElFromText(placeholder))),
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension),
tabIndentCompartment.current.of(
!disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension,
),
keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
),
...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 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 100 ms.
forceParsing(view, 9e6, 100);
cm.current = { view, languageCompartment };
if (autoFocus) {
view.focus();
}
if (autoSelect) {
view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
}
});
const extensions = [
languageCompartment.of(langExt),
placeholderCompartment.current.of(placeholderExt(placeholderElFromText(placeholder))),
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension),
tabIndentCompartment.current.of(
!disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension,
),
keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
),
...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 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 100 ms.
forceParsing(view, 9e6, 100);
cm.current = { view, languageCompartment };
if (autoFocus) {
view.focus();
}
if (autoSelect) {
view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
}
} catch (e) {
console.log('Failed to initialize Codemirror', e);
}

View File

@@ -5,10 +5,6 @@ import {
completionKeymap,
} from '@codemirror/autocomplete';
import { history, historyKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
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 {
codeFolding,
@@ -27,6 +23,7 @@ import {
crosshairCursor,
drawSelection,
dropCursor,
EditorView,
highlightActiveLineGutter,
highlightSpecialChars,
keymap,
@@ -36,15 +33,11 @@ import {
import { tags as t } from '@lezer/highlight';
import type { EnvironmentVariable } from '@yaakapp-internal/models';
import { graphql } from 'cm6-graphql';
import { EditorView } from 'codemirror';
import { pluralizeCount } from '../../../lib/pluralize';
import type { EditorProps } from './Editor';
import { pairs } from './pairs/extension';
import { text } from './text/extension';
import type { TwigCompletionOption } from './twig/completion';
import { twig } from './twig/extension';
import { pathParametersPlugin } from './twig/pathParameters';
import { url } from './url/extension';
export const syntaxHighlightStyle = HighlightStyle.define([
{
@@ -75,21 +68,25 @@ const syntaxTheme = EditorView.theme({}, { dark: true });
const closeBracketsExtensions: Extension = [closeBrackets(), keymap.of([...closeBracketsKeymap])];
const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSupport | null> = {
const syntaxExtensions: Record<
NonNullable<EditorProps['language']>,
null | (() => Promise<LanguageSupport>)
> = {
graphql: null,
json: json(),
javascript: javascript(),
html: xml(), // HTML as XML because HTML is oddly slow
xml: xml(),
url: url(),
pairs: pairs(),
text: text(),
markdown: markdown(),
json: () => import('@codemirror/lang-json').then((m) => m.json()),
javascript: () => import('@codemirror/lang-javascript').then((m) => m.javascript()),
// HTML as XML because HTML is oddly slow
html: () => import('@codemirror/lang-xml').then((m) => m.xml()),
xml: () => import('@codemirror/lang-xml').then((m) => m.xml()),
url: () => import('./url/extension').then((m) => m.url()),
pairs: () => import('./pairs/extension').then((m) => m.pairs()),
text: () => import('./text/extension').then((m) => m.text()),
markdown: () => import('@codemirror/lang-markdown').then((m) => m.markdown()),
};
const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript', 'graphql'];
export function getLanguageExtension({
export async function getLanguageExtension({
useTemplating,
language = 'text',
environmentVariables,
@@ -122,12 +119,14 @@ export function getLanguageExtension({
return [graphql(), extraExtensions];
}
const base = syntaxExtensions[language ?? 'text'] ?? text();
const base_ = syntaxExtensions[language ?? 'text'] ?? text();
const base = typeof base_ === 'function' ? await base_() : text();
if (!useTemplating) {
return [base, extraExtensions];
}
const { twig } = await import('./twig/extension');
return twig({
base,
environmentVariables,

View File

@@ -1,6 +1,5 @@
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, hoverTooltip, MatchDecorator, ViewPlugin } from '@codemirror/view';
import { EditorView } from 'codemirror';
import { Decoration, EditorView, hoverTooltip, MatchDecorator, ViewPlugin } from '@codemirror/view';
const REGEX =
/(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+*~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+*.~#?&/={}[\]]*))/g;

View File

@@ -1,8 +1,7 @@
import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
import { EditorView } from 'codemirror';
import { Decoration, ViewPlugin, WidgetType, EditorView } from '@codemirror/view';
class PathPlaceholderWidget extends WidgetType {
readonly #clickListenerCallback: () => void;

View File

@@ -1,9 +1,8 @@
import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
import { Decoration, ViewPlugin, WidgetType, EditorView } from '@codemirror/view';
import type { SyntaxNodeRef } from '@lezer/common';
import { EditorView } from 'codemirror';
import type { TwigCompletionOption } from './completion';
class TemplateTagWidget extends WidgetType {

View File

@@ -1,6 +1,6 @@
import type { EditorView } from '@codemirror/view';
import type { Color } from '@yaakapp/api';
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import type { ReactNode } from 'react';
import {
forwardRef,

View File

@@ -1,5 +1,5 @@
import type { EditorView } from '@codemirror/view';
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import {
forwardRef,
Fragment,

View File

@@ -7,6 +7,13 @@ import React, { useRef, useState } from 'react';
import { Document, Page } from 'react-pdf';
import { useContainerSize } from '../../hooks/useContainerQuery';
import('react-pdf').then(({ pdfjs }) => {
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
});
interface Props {
bodyPath: string;
}