mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-08 15:42:52 +02:00
Tackled remaining perf wins
This commit is contained in:
@@ -226,11 +226,13 @@ export const SidebarItem = memo(function SidebarItem({
|
|||||||
return (
|
return (
|
||||||
<li ref={ref} draggable>
|
<li ref={ref} draggable>
|
||||||
<div className={classNames(className, 'block relative group/item px-1.5 pb-0.5')}>
|
<div className={classNames(className, 'block relative group/item px-1.5 pb-0.5')}>
|
||||||
<SidebarItemContextMenu
|
{showContextMenu && (
|
||||||
child={child}
|
<SidebarItemContextMenu
|
||||||
show={showContextMenu}
|
child={child}
|
||||||
close={handleCloseContextMenu}
|
show={showContextMenu}
|
||||||
/>
|
close={handleCloseContextMenu}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
// tabIndex={-1} // Will prevent drag-n-drop
|
// tabIndex={-1} // Will prevent drag-n-drop
|
||||||
disabled={editing}
|
disabled={editing}
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ import { TemplateVariableDialog } from '../../TemplateVariableDialog';
|
|||||||
import { IconButton } from '../IconButton';
|
import { IconButton } from '../IconButton';
|
||||||
import { HStack } from '../Stacks';
|
import { HStack } from '../Stacks';
|
||||||
import './Editor.css';
|
import './Editor.css';
|
||||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
import {
|
||||||
|
baseExtensions,
|
||||||
|
emptyExtension,
|
||||||
|
getLanguageExtension,
|
||||||
|
multiLineExtensions,
|
||||||
|
} from './extensions';
|
||||||
import type { GenericCompletionConfig } from './genericCompletion';
|
import type { GenericCompletionConfig } from './genericCompletion';
|
||||||
import { singleLineExtensions } from './singleLine';
|
import { singleLineExtensions } from './singleLine';
|
||||||
|
|
||||||
@@ -166,9 +171,8 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
useEffect(
|
useEffect(
|
||||||
function configurePlaceholder() {
|
function configurePlaceholder() {
|
||||||
if (cm.current === null) return;
|
if (cm.current === null) return;
|
||||||
const effect = placeholderCompartment.current.reconfigure(
|
const ext = placeholderExt(placeholderElFromText(placeholder ?? ''));
|
||||||
placeholderExt(placeholderElFromText(placeholder ?? '')),
|
const effect = placeholderCompartment.current.reconfigure(ext);
|
||||||
);
|
|
||||||
cm.current?.view.dispatch({ effects: effect });
|
cm.current?.view.dispatch({ effects: effect });
|
||||||
},
|
},
|
||||||
[placeholder],
|
[placeholder],
|
||||||
@@ -179,7 +183,12 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
useEffect(
|
useEffect(
|
||||||
function configureWrapLines() {
|
function configureWrapLines() {
|
||||||
if (cm.current === null) return;
|
if (cm.current === null) return;
|
||||||
const ext = wrapLines ? [EditorView.lineWrapping] : [];
|
const current = wrapLinesCompartment.current.get(cm.current.view.state) ?? emptyExtension;
|
||||||
|
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
|
||||||
|
if (wrapLines && current !== emptyExtension) return; // Nothing to do
|
||||||
|
if (!wrapLines && current === emptyExtension) return; // Nothing to do
|
||||||
|
|
||||||
|
const ext = wrapLines ? EditorView.lineWrapping : emptyExtension;
|
||||||
const effect = wrapLinesCompartment.current.reconfigure(ext);
|
const effect = wrapLinesCompartment.current.reconfigure(ext);
|
||||||
cm.current?.view.dispatch({ effects: effect });
|
cm.current?.view.dispatch({ effects: effect });
|
||||||
},
|
},
|
||||||
@@ -323,7 +332,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
placeholderCompartment.current.of(
|
placeholderCompartment.current.of(
|
||||||
placeholderExt(placeholderElFromText(placeholder ?? '')),
|
placeholderExt(placeholderElFromText(placeholder ?? '')),
|
||||||
),
|
),
|
||||||
wrapLinesCompartment.current.of(wrapLines ? [EditorView.lineWrapping] : []),
|
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension),
|
||||||
...getExtensions({
|
...getExtensions({
|
||||||
container,
|
container,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { lintKeymap } from '@codemirror/lint';
|
import { lintKeymap } from '@codemirror/lint';
|
||||||
|
|
||||||
import { searchKeymap } from '@codemirror/search';
|
import { searchKeymap } from '@codemirror/search';
|
||||||
|
import type { Extension } from '@codemirror/state';
|
||||||
import { EditorState } from '@codemirror/state';
|
import { EditorState } from '@codemirror/state';
|
||||||
import {
|
import {
|
||||||
crosshairCursor,
|
crosshairCursor,
|
||||||
@@ -85,6 +86,8 @@ const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSup
|
|||||||
markdown: markdown(),
|
markdown: markdown(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const emptyExtension: Extension = [];
|
||||||
|
|
||||||
export function getLanguageExtension({
|
export function getLanguageExtension({
|
||||||
language,
|
language,
|
||||||
useTemplating = false,
|
useTemplating = false,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { deepEqual } from '@tanstack/react-router';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { EditorView } from 'codemirror';
|
import type { EditorView } from 'codemirror';
|
||||||
import {
|
import {
|
||||||
@@ -14,9 +13,11 @@ import {
|
|||||||
import type { XYCoord } from 'react-dnd';
|
import type { XYCoord } from 'react-dnd';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { usePrompt } from '../../hooks/usePrompt';
|
import { usePrompt } from '../../hooks/usePrompt';
|
||||||
|
import { useToggle } from '../../hooks/useToggle';
|
||||||
import { generateId } from '../../lib/generateId';
|
import { generateId } from '../../lib/generateId';
|
||||||
import { DropMarker } from '../DropMarker';
|
import { DropMarker } from '../DropMarker';
|
||||||
import { SelectFile } from '../SelectFile';
|
import { SelectFile } from '../SelectFile';
|
||||||
|
import { Button } from './Button';
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
import type { DropdownItem } from './Dropdown';
|
import type { DropdownItem } from './Dropdown';
|
||||||
import { Dropdown } from './Dropdown';
|
import { Dropdown } from './Dropdown';
|
||||||
@@ -62,6 +63,9 @@ export type Pair = {
|
|||||||
readOnlyName?: boolean;
|
readOnlyName?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Max number of pairs to show before prompting the user to reveal the rest */
|
||||||
|
const MAX_INITIAL_PAIRS = 50;
|
||||||
|
|
||||||
export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function PairEditor(
|
export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function PairEditor(
|
||||||
{
|
{
|
||||||
stateKey,
|
stateKey,
|
||||||
@@ -86,12 +90,8 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
|||||||
const [forceFocusNamePairId, setForceFocusNamePairId] = useState<string | null>(null);
|
const [forceFocusNamePairId, setForceFocusNamePairId] = useState<string | null>(null);
|
||||||
const [forceFocusValuePairId, setForceFocusValuePairId] = useState<string | null>(null);
|
const [forceFocusValuePairId, setForceFocusValuePairId] = useState<string | null>(null);
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
const [pairs, setPairs] = useState<Pair[]>(() => {
|
const [pairs, setPairs] = useState<Pair[]>([]);
|
||||||
// Remove empty headers on initial render
|
const [showAll, toggleShowAll] = useToggle(false);
|
||||||
const nonEmpty = originalPairs.filter((h) => !(h.name === '' && h.value === ''));
|
|
||||||
const pairs = nonEmpty.map((pair) => ensureValidPair(pair));
|
|
||||||
return [...pairs, ensureValidPair()];
|
|
||||||
});
|
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
@@ -105,17 +105,24 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Remove empty headers on initial render
|
// Remove empty headers on initial render and ensure they all have valid ids (pairs didn't used to have IDs)
|
||||||
// TODO: Make this not refresh the entire editor when forceUpdateKey changes, using some
|
const newPairs = [];
|
||||||
// sort of diff method or deterministic IDs based on array index and update key
|
for (let i = 0; i < originalPairs.length; i++) {
|
||||||
const nonEmpty = originalPairs.filter(
|
const p = originalPairs[i];
|
||||||
(h, i) => i !== originalPairs.length - 1 && !(h.name === '' && h.value === ''),
|
if (!p) continue; // Make TS happy
|
||||||
);
|
if (isPairEmpty(p)) continue;
|
||||||
const newPairs = nonEmpty.map((pair) => ensureValidPair(pair));
|
if (!p.id) p.id = generateId();
|
||||||
if (!deepEqual(pairs, newPairs)) {
|
newPairs.push(p);
|
||||||
setPairs(pairs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add empty last pair if there is none
|
||||||
|
const lastPair = newPairs[newPairs.length - 1];
|
||||||
|
if (lastPair != null && !isPairEmpty(lastPair)) {
|
||||||
|
newPairs.push(emptyPair());
|
||||||
|
}
|
||||||
|
|
||||||
|
setPairs(newPairs);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [forceUpdateKey]);
|
}, [forceUpdateKey]);
|
||||||
|
|
||||||
@@ -181,10 +188,9 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
|||||||
setForceFocusValuePairId(null); // Remove focus override when something focused
|
setForceFocusValuePairId(null); // Remove focus override when something focused
|
||||||
const isLast = pair.id === pairs[pairs.length - 1]?.id;
|
const isLast = pair.id === pairs[pairs.length - 1]?.id;
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
const newPair = ensureValidPair();
|
|
||||||
const prevPair = pairs[pairs.length - 1];
|
const prevPair = pairs[pairs.length - 1];
|
||||||
setForceFocusNamePairId(prevPair?.id ?? null);
|
setForceFocusNamePairId(prevPair?.id ?? null);
|
||||||
return [...pairs, newPair];
|
return [...pairs, emptyPair()];
|
||||||
} else {
|
} else {
|
||||||
return pairs;
|
return pairs;
|
||||||
}
|
}
|
||||||
@@ -192,13 +198,6 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ensure there's always at least one pair
|
|
||||||
useEffect(() => {
|
|
||||||
if (pairs.length === 0) {
|
|
||||||
setPairs((pairs) => [...pairs, ensureValidPair()]);
|
|
||||||
}
|
|
||||||
}, [pairs]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -213,6 +212,8 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{pairs.map((p, i) => {
|
{pairs.map((p, i) => {
|
||||||
|
if (!showAll && i > MAX_INITIAL_PAIRS) return null;
|
||||||
|
|
||||||
const isLast = i === pairs.length - 1;
|
const isLast = i === pairs.length - 1;
|
||||||
return (
|
return (
|
||||||
<Fragment key={p.id}>
|
<Fragment key={p.id}>
|
||||||
@@ -245,6 +246,17 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{!showAll && pairs.length > MAX_INITIAL_PAIRS && (
|
||||||
|
<Button
|
||||||
|
onClick={toggleShowAll}
|
||||||
|
variant="border"
|
||||||
|
className="m-2"
|
||||||
|
size="xs"
|
||||||
|
event="pairs.reveal-more"
|
||||||
|
>
|
||||||
|
Show {pairs.length - MAX_INITIAL_PAIRS} More
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -607,12 +619,15 @@ function FileActionsDropdown({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidPair(initialPair?: Pair): Pair {
|
function emptyPair(): Pair {
|
||||||
return {
|
return {
|
||||||
name: initialPair?.name ?? '',
|
enabled: true,
|
||||||
value: initialPair?.value ?? '',
|
name: '',
|
||||||
enabled: initialPair?.enabled ?? true,
|
value: '',
|
||||||
isFile: initialPair?.isFile ?? false,
|
id: generateId(),
|
||||||
id: initialPair?.id || generateId(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPairEmpty(pair: Pair): boolean {
|
||||||
|
return !pair.name && !pair.value;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
||||||
import { useMemo } from 'react';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
import { activeEnvironmentAtom } from './useActiveEnvironment';
|
||||||
import { useEnvironments } from './useEnvironments';
|
import { environmentsBreakdownAtom } from './useEnvironments';
|
||||||
|
|
||||||
|
const activeEnvironmentVariablesAtom = atom((get) => {
|
||||||
|
const { baseEnvironment } = get(environmentsBreakdownAtom);
|
||||||
|
const activeEnvironment = get(activeEnvironmentAtom);
|
||||||
|
|
||||||
|
const varMap: Record<string, EnvironmentVariable> = {};
|
||||||
|
const allVariables = [
|
||||||
|
...(baseEnvironment?.variables ?? []),
|
||||||
|
...(activeEnvironment?.variables ?? []),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const v of allVariables) {
|
||||||
|
if (!v.enabled || !v.name) continue;
|
||||||
|
varMap[v.name] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(varMap);
|
||||||
|
});
|
||||||
|
|
||||||
export function useActiveEnvironmentVariables() {
|
export function useActiveEnvironmentVariables() {
|
||||||
const { baseEnvironment } = useEnvironments();
|
return useAtomValue(activeEnvironmentVariablesAtom);
|
||||||
const [environment] = useActiveEnvironment();
|
|
||||||
|
|
||||||
const variables = useMemo(() => {
|
|
||||||
const varMap: Record<string, EnvironmentVariable> = {};
|
|
||||||
|
|
||||||
const allVariables = [...(baseEnvironment?.variables ?? []), ...(environment?.variables ?? [])];
|
|
||||||
|
|
||||||
for (const v of allVariables) {
|
|
||||||
if (!v.enabled || !v.name) continue;
|
|
||||||
varMap[v.name] = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(varMap);
|
|
||||||
}, [baseEnvironment?.variables, environment?.variables]);
|
|
||||||
|
|
||||||
return variables;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ import { atom } from 'jotai/index';
|
|||||||
|
|
||||||
export const environmentsAtom = atom<Environment[]>([]);
|
export const environmentsAtom = atom<Environment[]>([]);
|
||||||
|
|
||||||
export function useEnvironments() {
|
export const environmentsBreakdownAtom = atom<{
|
||||||
const allEnvironments = useAtomValue(environmentsAtom);
|
baseEnvironment: Environment | null;
|
||||||
|
allEnvironments: Environment[];
|
||||||
|
subEnvironments: Environment[];
|
||||||
|
}>((get) => {
|
||||||
|
const allEnvironments = get(environmentsAtom);
|
||||||
const baseEnvironment = allEnvironments.find((e) => e.environmentId == null) ?? null;
|
const baseEnvironment = allEnvironments.find((e) => e.environmentId == null) ?? null;
|
||||||
const subEnvironments =
|
const subEnvironments =
|
||||||
allEnvironments.filter((e) => e.environmentId === (baseEnvironment?.id ?? 'n/a')) ?? [];
|
allEnvironments.filter((e) => e.environmentId === (baseEnvironment?.id ?? 'n/a')) ?? [];
|
||||||
|
|
||||||
return { baseEnvironment, subEnvironments, allEnvironments } as const;
|
return { baseEnvironment, subEnvironments, allEnvironments } as const;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useEnvironments() {
|
||||||
|
return useAtomValue(environmentsBreakdownAtom);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user