mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-27 20:01:10 +01:00
Run oxfmt across repo, add format script and docs
Add .oxfmtignore to skip generated bindings and wasm-pack output. Add npm format script, update DEVELOPMENT.md for Vite+ toolchain, and format all non-generated files with oxfmt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { type DecorationSet, MatchDecorator, type ViewUpdate } from '@codemirror/view';
|
||||
import { type DecorationSet, MatchDecorator, type ViewUpdate } from "@codemirror/view";
|
||||
|
||||
/**
|
||||
* This is a custom MatchDecorator that will not decorate a match if the selection is inside it
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { yaml } from '@codemirror/lang-yaml';
|
||||
import { syntaxHighlighting } from '@codemirror/language';
|
||||
import { MergeView } from '@codemirror/merge';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import './DiffViewer.css';
|
||||
import { readonlyExtensions, syntaxHighlightStyle } from './extensions';
|
||||
import { yaml } from "@codemirror/lang-yaml";
|
||||
import { syntaxHighlighting } from "@codemirror/language";
|
||||
import { MergeView } from "@codemirror/merge";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useRef } from "react";
|
||||
import "./DiffViewer.css";
|
||||
import { readonlyExtensions, syntaxHighlightStyle } from "./extensions";
|
||||
|
||||
interface Props {
|
||||
/** Original/previous version (left side) */
|
||||
@@ -45,7 +45,7 @@ export function DiffViewer({ original, modified, className }: Props) {
|
||||
collapseUnchanged: { margin: 2, minSize: 3 },
|
||||
highlightChanges: false,
|
||||
gutter: true,
|
||||
orientation: 'a-b',
|
||||
orientation: "a-b",
|
||||
revertControls: undefined,
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ export function DiffViewer({ original, modified, className }: Props) {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classNames('cm-wrapper cm-multiline h-full w-full', className)}
|
||||
className={classNames("cm-wrapper cm-multiline h-full w-full", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@
|
||||
}
|
||||
|
||||
input.cm-textfield {
|
||||
@apply cursor-text;
|
||||
@apply cursor-text;
|
||||
}
|
||||
|
||||
.cm-search label {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { startCompletion } from '@codemirror/autocomplete';
|
||||
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 { EditorView, keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { startCompletion } from "@codemirror/autocomplete";
|
||||
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 { 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 } from '@yaakapp-internal/models';
|
||||
import { settingsAtom } from '@yaakapp-internal/models';
|
||||
import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { md5 } from 'js-md5';
|
||||
import type { ReactNode, RefObject } from 'react';
|
||||
import { vscodeKeymap } from "@replit/codemirror-vscode-keymap";
|
||||
import type { EditorKeymap } from "@yaakapp-internal/models";
|
||||
import { settingsAtom } from "@yaakapp-internal/models";
|
||||
import type { EditorLanguage, TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import type { GraphQLSchema } from "graphql";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { md5 } from "js-md5";
|
||||
import type { ReactNode, RefObject } from "react";
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
@@ -25,32 +25,32 @@ import {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { activeEnvironmentAtom } from '../../../hooks/useActiveEnvironment';
|
||||
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
|
||||
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
|
||||
import { eventMatchesHotkey } from '../../../hooks/useHotKey';
|
||||
import { useRequestEditor } from '../../../hooks/useRequestEditor';
|
||||
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
|
||||
import { editEnvironment } from '../../../lib/editEnvironment';
|
||||
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption';
|
||||
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { HStack } from '../Stacks';
|
||||
import './Editor.css';
|
||||
} from "react";
|
||||
import { activeEnvironmentAtom } from "../../../hooks/useActiveEnvironment";
|
||||
import type { WrappedEnvironmentVariable } from "../../../hooks/useEnvironmentVariables";
|
||||
import { useEnvironmentVariables } from "../../../hooks/useEnvironmentVariables";
|
||||
import { eventMatchesHotkey } from "../../../hooks/useHotKey";
|
||||
import { useRequestEditor } from "../../../hooks/useRequestEditor";
|
||||
import { useTemplateFunctionCompletionOptions } from "../../../hooks/useTemplateFunctions";
|
||||
import { editEnvironment } from "../../../lib/editEnvironment";
|
||||
import { tryFormatJson, tryFormatXml } from "../../../lib/formatters";
|
||||
import { jotaiStore } from "../../../lib/jotai";
|
||||
import { withEncryptionEnabled } from "../../../lib/setupOrConfigureEncryption";
|
||||
import { TemplateFunctionDialog } from "../../TemplateFunctionDialog";
|
||||
import { IconButton } from "../IconButton";
|
||||
import { HStack } from "../Stacks";
|
||||
import "./Editor.css";
|
||||
import {
|
||||
baseExtensions,
|
||||
getLanguageExtension,
|
||||
multiLineExtensions,
|
||||
readonlyExtensions,
|
||||
} from './extensions';
|
||||
import type { GenericCompletionConfig } from './genericCompletion';
|
||||
import { singleLineExtensions } from './singleLine';
|
||||
} from "./extensions";
|
||||
import type { GenericCompletionConfig } from "./genericCompletion";
|
||||
import { singleLineExtensions } from "./singleLine";
|
||||
|
||||
// VSCode's Tab actions mess with the single-line editor tab actions, so remove it.
|
||||
const vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== 'Tab');
|
||||
const vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== "Tab");
|
||||
|
||||
const keymapExtensions: Record<EditorKeymap, Extension> = {
|
||||
vim: vim(),
|
||||
@@ -74,10 +74,10 @@ export interface EditorProps {
|
||||
forcedEnvironmentId?: string;
|
||||
forceUpdateKey?: string | number;
|
||||
format?: (v: string) => Promise<string>;
|
||||
heightMode?: 'auto' | 'full';
|
||||
heightMode?: "auto" | "full";
|
||||
hideGutter?: boolean;
|
||||
id?: string;
|
||||
language?: EditorLanguage | 'pairs' | 'url' | 'timeline' | null;
|
||||
language?: EditorLanguage | "pairs" | "url" | "timeline" | null;
|
||||
lintExtension?: Extension;
|
||||
graphQLSchema?: GraphQLSchema | null;
|
||||
onBlur?: () => void;
|
||||
@@ -92,7 +92,7 @@ export interface EditorProps {
|
||||
containerOnly?: boolean;
|
||||
stateKey: string | null;
|
||||
tooltipContainer?: HTMLElement;
|
||||
type?: 'text' | 'password';
|
||||
type?: "text" | "password";
|
||||
wrapLines?: boolean;
|
||||
setRef?: (view: EditorView | null) => void;
|
||||
}
|
||||
@@ -147,7 +147,7 @@ function EditorInner({
|
||||
const useTemplating = !!(autocompleteFunctions || autocompleteVariables || autocomplete);
|
||||
const environmentVariables = useMemo(() => {
|
||||
if (!autocompleteVariables) return emptyVariables;
|
||||
return typeof autocompleteVariables === 'function'
|
||||
return typeof autocompleteVariables === "function"
|
||||
? allEnvironmentVariables.filter(autocompleteVariables)
|
||||
: allEnvironmentVariables;
|
||||
}, [allEnvironmentVariables, autocompleteVariables]);
|
||||
@@ -163,18 +163,18 @@ function EditorInner({
|
||||
if (
|
||||
singleLine ||
|
||||
language == null ||
|
||||
language === 'text' ||
|
||||
language === 'url' ||
|
||||
language === 'pairs'
|
||||
language === "text" ||
|
||||
language === "url" ||
|
||||
language === "pairs"
|
||||
) {
|
||||
disableTabIndent = true;
|
||||
}
|
||||
|
||||
if (format == null && !readOnly) {
|
||||
format =
|
||||
language === 'json'
|
||||
language === "json"
|
||||
? tryFormatJson
|
||||
: language === 'xml' || language === 'html'
|
||||
: language === "xml" || language === "html"
|
||||
? tryFormatXml
|
||||
: undefined;
|
||||
}
|
||||
@@ -182,37 +182,37 @@ function EditorInner({
|
||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handleChange = useRef<EditorProps['onChange']>(onChange);
|
||||
const handleChange = useRef<EditorProps["onChange"]>(onChange);
|
||||
useEffect(() => {
|
||||
handleChange.current = onChange;
|
||||
}, [onChange]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handlePaste = useRef<EditorProps['onPaste']>(onPaste);
|
||||
const handlePaste = useRef<EditorProps["onPaste"]>(onPaste);
|
||||
useEffect(() => {
|
||||
handlePaste.current = onPaste;
|
||||
}, [onPaste]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handlePasteOverwrite = useRef<EditorProps['onPasteOverwrite']>(onPasteOverwrite);
|
||||
const handlePasteOverwrite = useRef<EditorProps["onPasteOverwrite"]>(onPasteOverwrite);
|
||||
useEffect(() => {
|
||||
handlePasteOverwrite.current = onPasteOverwrite;
|
||||
}, [onPasteOverwrite]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handleFocus = useRef<EditorProps['onFocus']>(onFocus);
|
||||
const handleFocus = useRef<EditorProps["onFocus"]>(onFocus);
|
||||
useEffect(() => {
|
||||
handleFocus.current = onFocus;
|
||||
}, [onFocus]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handleBlur = useRef<EditorProps['onBlur']>(onBlur);
|
||||
const handleBlur = useRef<EditorProps["onBlur"]>(onBlur);
|
||||
useEffect(() => {
|
||||
handleBlur.current = onBlur;
|
||||
}, [onBlur]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handleKeyDown = useRef<EditorProps['onKeyDown']>(onKeyDown);
|
||||
const handleKeyDown = useRef<EditorProps["onKeyDown"]>(onKeyDown);
|
||||
useEffect(() => {
|
||||
handleKeyDown.current = onKeyDown;
|
||||
}, [onKeyDown]);
|
||||
@@ -236,10 +236,10 @@ function EditorInner({
|
||||
if (cm.current === null) return;
|
||||
const current = keymapCompartment.current.get(cm.current.view.state) ?? [];
|
||||
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
|
||||
if (settings.editorKeymap === 'default' && current === keymapExtensions.default) return; // Nothing to do
|
||||
if (settings.editorKeymap === 'vim' && current === keymapExtensions.vim) return; // Nothing to do
|
||||
if (settings.editorKeymap === 'vscode' && current === keymapExtensions.vscode) return; // Nothing to do
|
||||
if (settings.editorKeymap === 'emacs' && current === keymapExtensions.emacs) return; // Nothing to do
|
||||
if (settings.editorKeymap === "default" && current === keymapExtensions.default) return; // Nothing to do
|
||||
if (settings.editorKeymap === "vim" && current === keymapExtensions.vim) return; // Nothing to do
|
||||
if (settings.editorKeymap === "vscode" && current === keymapExtensions.vscode) return; // Nothing to do
|
||||
if (settings.editorKeymap === "emacs" && current === keymapExtensions.emacs) return; // Nothing to do
|
||||
|
||||
const ext = keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default;
|
||||
const effects = keymapCompartment.current.reconfigure(ext);
|
||||
@@ -289,7 +289,7 @@ function EditorInner({
|
||||
TemplateFunctionDialog.show(fn, tagValue, startPos, cm.current.view);
|
||||
};
|
||||
|
||||
if (fn.name === 'secure') {
|
||||
if (fn.name === "secure") {
|
||||
withEncryptionEnabled(show);
|
||||
} else {
|
||||
show();
|
||||
@@ -309,7 +309,7 @@ function EditorInner({
|
||||
const onClickMissingVariable = useCallback(async (name: string) => {
|
||||
const activeEnvironment = jotaiStore.get(activeEnvironmentAtom);
|
||||
await editEnvironment(activeEnvironment, {
|
||||
addOrFocusVariable: { name, value: '', enabled: true },
|
||||
addOrFocusVariable: { name, value: "", enabled: true },
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -414,9 +414,9 @@ function EditorInner({
|
||||
: []),
|
||||
];
|
||||
|
||||
const cachedJsonState = getCachedEditorState(defaultValue ?? '', stateKey);
|
||||
const cachedJsonState = getCachedEditorState(defaultValue ?? "", stateKey);
|
||||
|
||||
const doc = `${defaultValue ?? ''}`;
|
||||
const doc = `${defaultValue ?? ""}`;
|
||||
const config: EditorStateConfig = { extensions, doc };
|
||||
|
||||
const state = cachedJsonState
|
||||
@@ -439,7 +439,7 @@ function EditorInner({
|
||||
}
|
||||
setRef?.(view);
|
||||
} catch (e) {
|
||||
console.log('Failed to initialize Codemirror', e);
|
||||
console.log("Failed to initialize Codemirror", e);
|
||||
}
|
||||
},
|
||||
[forceUpdateKey],
|
||||
@@ -449,7 +449,7 @@ function EditorInner({
|
||||
useEffect(
|
||||
function updateReadOnlyEditor() {
|
||||
if (readOnly && cm.current?.view != null) {
|
||||
updateContents(cm.current.view, defaultValue || '');
|
||||
updateContents(cm.current.view, defaultValue || "");
|
||||
}
|
||||
},
|
||||
[defaultValue, readOnly],
|
||||
@@ -460,7 +460,7 @@ function EditorInner({
|
||||
function updateNonFocusedEditor() {
|
||||
const notFocused = !cm.current?.view.hasFocus;
|
||||
if (notFocused && cm.current != null) {
|
||||
updateContents(cm.current.view, defaultValue || '');
|
||||
updateContents(cm.current.view, defaultValue || "");
|
||||
}
|
||||
},
|
||||
[defaultValue],
|
||||
@@ -470,7 +470,7 @@ function EditorInner({
|
||||
const decoratedActions = useMemo(() => {
|
||||
const results = [];
|
||||
const actionClassName = classNames(
|
||||
'bg-surface transition-opacity transform-gpu opacity-0 group-hover:opacity-100 hover:!opacity-100 shadow',
|
||||
"bg-surface transition-opacity transform-gpu opacity-0 group-hover:opacity-100 hover:!opacity-100 shadow",
|
||||
);
|
||||
|
||||
if (format) {
|
||||
@@ -517,12 +517,12 @@ function EditorInner({
|
||||
ref={initEditorRef}
|
||||
className={classNames(
|
||||
className,
|
||||
'cm-wrapper text-base',
|
||||
disabled && 'opacity-disabled',
|
||||
type === 'password' && 'cm-obscure-text',
|
||||
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
|
||||
singleLine ? 'cm-singleline' : 'cm-multiline',
|
||||
readOnly && 'cm-readonly',
|
||||
"cm-wrapper text-base",
|
||||
disabled && "opacity-disabled",
|
||||
type === "password" && "cm-obscure-text",
|
||||
heightMode === "auto" ? "cm-auto-height" : "cm-full-height",
|
||||
singleLine ? "cm-singleline" : "cm-multiline",
|
||||
readOnly && "cm-readonly",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -539,8 +539,8 @@ function EditorInner({
|
||||
space={1}
|
||||
justifyContent="end"
|
||||
className={classNames(
|
||||
'absolute bottom-2 left-0 right-0',
|
||||
'pointer-events-none', // No pointer events, so we don't block the editor
|
||||
"absolute bottom-2 left-0 right-0",
|
||||
"pointer-events-none", // No pointer events, so we don't block the editor
|
||||
)}
|
||||
>
|
||||
{decoratedActions}
|
||||
@@ -562,20 +562,20 @@ function getExtensions({
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
}: Pick<EditorProps, 'singleLine' | 'readOnly' | 'hideGutter'> & {
|
||||
stateKey: EditorProps['stateKey'];
|
||||
}: Pick<EditorProps, "singleLine" | "readOnly" | "hideGutter"> & {
|
||||
stateKey: EditorProps["stateKey"];
|
||||
container: HTMLDivElement | null;
|
||||
onChange: RefObject<EditorProps['onChange']>;
|
||||
onPaste: RefObject<EditorProps['onPaste']>;
|
||||
onPasteOverwrite: RefObject<EditorProps['onPasteOverwrite']>;
|
||||
onFocus: RefObject<EditorProps['onFocus']>;
|
||||
onBlur: RefObject<EditorProps['onBlur']>;
|
||||
onKeyDown: RefObject<EditorProps['onKeyDown']>;
|
||||
onChange: RefObject<EditorProps["onChange"]>;
|
||||
onPaste: RefObject<EditorProps["onPaste"]>;
|
||||
onPasteOverwrite: RefObject<EditorProps["onPasteOverwrite"]>;
|
||||
onFocus: RefObject<EditorProps["onFocus"]>;
|
||||
onBlur: RefObject<EditorProps["onBlur"]>;
|
||||
onKeyDown: RefObject<EditorProps["onKeyDown"]>;
|
||||
}) {
|
||||
// TODO: Ensure tooltips render inside the dialog if we are in one.
|
||||
const parent =
|
||||
container?.closest<HTMLDivElement>('[role="dialog"]') ??
|
||||
document.querySelector<HTMLDivElement>('#cm-portal') ??
|
||||
document.querySelector<HTMLDivElement>("#cm-portal") ??
|
||||
undefined;
|
||||
|
||||
return [
|
||||
@@ -589,7 +589,7 @@ function getExtensions({
|
||||
},
|
||||
keydown: (e, view) => {
|
||||
// Check if the hotkey matches the editor.autocomplete action
|
||||
if (eventMatchesHotkey(e, 'editor.autocomplete')) {
|
||||
if (eventMatchesHotkey(e, "editor.autocomplete")) {
|
||||
e.preventDefault();
|
||||
startCompletion(view);
|
||||
return true;
|
||||
@@ -597,7 +597,7 @@ function getExtensions({
|
||||
onKeyDown.current?.(e);
|
||||
},
|
||||
paste: (e, v) => {
|
||||
const textData = e.clipboardData?.getData('text/plain') ?? '';
|
||||
const textData = e.clipboardData?.getData("text/plain") ?? "";
|
||||
onPaste.current?.(textData);
|
||||
if (v.state.selection.main.from === 0 && v.state.selection.main.to === v.state.doc.length) {
|
||||
onPasteOverwrite.current?.(e, textData);
|
||||
@@ -605,7 +605,7 @@ function getExtensions({
|
||||
},
|
||||
}),
|
||||
tooltips({ parent }),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== "Enter") : defaultKeymap),
|
||||
...(singleLine ? [singleLineExtensions()] : []),
|
||||
...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
|
||||
...(readOnly ? readonlyExtensions : []),
|
||||
@@ -627,10 +627,10 @@ function getExtensions({
|
||||
}
|
||||
|
||||
const placeholderElFromText = (text: string | undefined) => {
|
||||
const el = document.createElement('div');
|
||||
const el = document.createElement("div");
|
||||
// Default to <SPACE> because codemirror needs it for sizing. I'm not sure why, but probably something
|
||||
// to do with how Yaak "hacks" it with CSS for single line input.
|
||||
el.innerHTML = text ? text.replaceAll('\n', '<br/>') : ' ';
|
||||
el.innerHTML = text ? text.replaceAll("\n", "<br/>") : " ";
|
||||
return el;
|
||||
};
|
||||
|
||||
@@ -646,7 +646,7 @@ function saveCachedEditorState(stateKey: string | null, state: EditorState | nul
|
||||
try {
|
||||
sessionStorage.setItem(computeFullStateKey(stateKey), JSON.stringify(stateObj));
|
||||
} catch (err) {
|
||||
console.log('Failed to save to editor state', stateKey, err);
|
||||
console.log("Failed to save to editor state", stateKey, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,7 +667,7 @@ function getCachedEditorState(doc: string, stateKey: string | null) {
|
||||
state.doc = doc;
|
||||
return state;
|
||||
} catch (err) {
|
||||
console.log('Failed to restore editor storage', stateKey, err);
|
||||
console.log("Failed to restore editor storage", stateKey, err);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import type { EditorProps } from './Editor';
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { EditorProps } from "./Editor";
|
||||
|
||||
const Editor_ = lazy(() => import('./Editor').then((m) => ({ default: m.Editor })));
|
||||
const Editor_ = lazy(() => import("./Editor").then((m) => ({ default: m.Editor })));
|
||||
|
||||
export function Editor(props: EditorProps) {
|
||||
return (
|
||||
|
||||
@@ -3,15 +3,15 @@ import {
|
||||
closeBrackets,
|
||||
closeBracketsKeymap,
|
||||
completionKeymap,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { history, historyKeymap } from '@codemirror/commands';
|
||||
import { go } from '@codemirror/lang-go';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { php } from '@codemirror/lang-php';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
} from "@codemirror/autocomplete";
|
||||
import { history, historyKeymap } from "@codemirror/commands";
|
||||
import { go } from "@codemirror/lang-go";
|
||||
import { java } from "@codemirror/lang-java";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { php } from "@codemirror/lang-php";
|
||||
import { python } from "@codemirror/lang-python";
|
||||
import { xml } from "@codemirror/lang-xml";
|
||||
import {
|
||||
bracketMatching,
|
||||
codeFolding,
|
||||
@@ -22,20 +22,20 @@ import {
|
||||
LanguageSupport,
|
||||
StreamLanguage,
|
||||
syntaxHighlighting,
|
||||
} from '@codemirror/language';
|
||||
import { c, csharp, kotlin, objectiveC } from '@codemirror/legacy-modes/mode/clike';
|
||||
import { clojure } from '@codemirror/legacy-modes/mode/clojure';
|
||||
import { http } from '@codemirror/legacy-modes/mode/http';
|
||||
import { oCaml } from '@codemirror/legacy-modes/mode/mllike';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { r } from '@codemirror/legacy-modes/mode/r';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { linter, lintGutter, lintKeymap } from '@codemirror/lint';
|
||||
import { search, searchKeymap } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
} from "@codemirror/language";
|
||||
import { c, csharp, kotlin, objectiveC } from "@codemirror/legacy-modes/mode/clike";
|
||||
import { clojure } from "@codemirror/legacy-modes/mode/clojure";
|
||||
import { http } from "@codemirror/legacy-modes/mode/http";
|
||||
import { oCaml } from "@codemirror/legacy-modes/mode/mllike";
|
||||
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
|
||||
import { r } from "@codemirror/legacy-modes/mode/r";
|
||||
import { ruby } from "@codemirror/legacy-modes/mode/ruby";
|
||||
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||
import { swift } from "@codemirror/legacy-modes/mode/swift";
|
||||
import { linter, lintGutter, lintKeymap } from "@codemirror/lint";
|
||||
import { search, searchKeymap } from "@codemirror/search";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import {
|
||||
crosshairCursor,
|
||||
drawSelection,
|
||||
@@ -46,51 +46,51 @@ import {
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
} from '@codemirror/view';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { jsonc, jsoncLanguage } from '@shopify/lang-jsonc';
|
||||
import { graphql } from 'cm6-graphql';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId';
|
||||
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import { renderMarkdown } from '../../../lib/markdown';
|
||||
import { pluralizeCount } from '../../../lib/pluralize';
|
||||
import { showGraphQLDocExplorerAtom } from '../../graphql/graphqlAtoms';
|
||||
import type { EditorProps } from './Editor';
|
||||
import { jsonParseLinter } from './json-lint';
|
||||
import { pairs } from './pairs/extension';
|
||||
import { searchMatchCount } from './searchMatchCount';
|
||||
import { text } from './text/extension';
|
||||
import { timeline } from './timeline/extension';
|
||||
import type { TwigCompletionOption } from './twig/completion';
|
||||
import { twig } from './twig/extension';
|
||||
import { pathParametersPlugin } from './twig/pathParameters';
|
||||
import { url } from './url/extension';
|
||||
} from "@codemirror/view";
|
||||
import { tags as t } from "@lezer/highlight";
|
||||
import { jsonc, jsoncLanguage } from "@shopify/lang-jsonc";
|
||||
import { graphql } from "cm6-graphql";
|
||||
import type { GraphQLSchema } from "graphql";
|
||||
import { activeRequestIdAtom } from "../../../hooks/useActiveRequestId";
|
||||
import type { WrappedEnvironmentVariable } from "../../../hooks/useEnvironmentVariables";
|
||||
import { jotaiStore } from "../../../lib/jotai";
|
||||
import { renderMarkdown } from "../../../lib/markdown";
|
||||
import { pluralizeCount } from "../../../lib/pluralize";
|
||||
import { showGraphQLDocExplorerAtom } from "../../graphql/graphqlAtoms";
|
||||
import type { EditorProps } from "./Editor";
|
||||
import { jsonParseLinter } from "./json-lint";
|
||||
import { pairs } from "./pairs/extension";
|
||||
import { searchMatchCount } from "./searchMatchCount";
|
||||
import { text } from "./text/extension";
|
||||
import { timeline } from "./timeline/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([
|
||||
{
|
||||
tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment],
|
||||
color: 'var(--textSubtlest)',
|
||||
color: "var(--textSubtlest)",
|
||||
},
|
||||
{
|
||||
tag: [t.emphasis],
|
||||
textDecoration: 'underline',
|
||||
textDecoration: "underline",
|
||||
},
|
||||
{
|
||||
tag: [t.angleBracket, t.paren, t.bracket, t.squareBracket, t.brace, t.separator, t.punctuation],
|
||||
color: 'var(--textSubtle)',
|
||||
color: "var(--textSubtle)",
|
||||
},
|
||||
{
|
||||
tag: [t.link, t.name, t.tagName, t.angleBracket, t.docString, t.number],
|
||||
color: 'var(--info)',
|
||||
color: "var(--info)",
|
||||
},
|
||||
{ tag: [t.variableName], color: 'var(--success)' },
|
||||
{ tag: [t.bool], color: 'var(--warning)' },
|
||||
{ tag: [t.attributeName, t.propertyName], color: 'var(--primary)' },
|
||||
{ tag: [t.attributeValue], color: 'var(--warning)' },
|
||||
{ tag: [t.string], color: 'var(--notice)' },
|
||||
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: 'var(--danger)' },
|
||||
{ tag: [t.variableName], color: "var(--success)" },
|
||||
{ tag: [t.bool], color: "var(--warning)" },
|
||||
{ tag: [t.attributeName, t.propertyName], color: "var(--primary)" },
|
||||
{ tag: [t.attributeValue], color: "var(--warning)" },
|
||||
{ tag: [t.string], color: "var(--notice)" },
|
||||
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: "var(--danger)" },
|
||||
]);
|
||||
|
||||
const syntaxTheme = EditorView.theme({}, { dark: true });
|
||||
@@ -102,7 +102,7 @@ const legacyLang = (mode: Parameters<typeof StreamLanguage.define>[0]) => {
|
||||
};
|
||||
|
||||
const syntaxExtensions: Record<
|
||||
NonNullable<EditorProps['language']>,
|
||||
NonNullable<EditorProps["language"]>,
|
||||
null | (() => LanguageSupport)
|
||||
> = {
|
||||
graphql: null,
|
||||
@@ -134,11 +134,11 @@ const syntaxExtensions: Record<
|
||||
swift: legacyLang(swift),
|
||||
};
|
||||
|
||||
const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript', 'graphql'];
|
||||
const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ["json", "javascript", "graphql"];
|
||||
|
||||
export function getLanguageExtension({
|
||||
useTemplating,
|
||||
language = 'text',
|
||||
language = "text",
|
||||
lintExtension,
|
||||
environmentVariables,
|
||||
autocomplete,
|
||||
@@ -156,10 +156,10 @@ export function getLanguageExtension({
|
||||
onClickPathParameter: (name: string) => void;
|
||||
completionOptions: TwigCompletionOption[];
|
||||
graphQLSchema: GraphQLSchema | null;
|
||||
} & Pick<EditorProps, 'language' | 'autocomplete' | 'hideGutter' | 'lintExtension'>) {
|
||||
} & Pick<EditorProps, "language" | "autocomplete" | "hideGutter" | "lintExtension">) {
|
||||
const extraExtensions: Extension[] = [];
|
||||
|
||||
if (language === 'url') {
|
||||
if (language === "url") {
|
||||
extraExtensions.push(pathParametersPlugin(onClickPathParameter));
|
||||
}
|
||||
|
||||
@@ -169,13 +169,13 @@ export function getLanguageExtension({
|
||||
}
|
||||
|
||||
// GraphQL is a special exception
|
||||
if (language === 'graphql') {
|
||||
if (language === "graphql") {
|
||||
return [
|
||||
graphql(graphQLSchema ?? undefined, {
|
||||
async onCompletionInfoRender(gqlCompletionItem): Promise<Node | null> {
|
||||
if (!gqlCompletionItem.documentation) return null;
|
||||
const innerHTML = await renderMarkdown(gqlCompletionItem.documentation);
|
||||
const span = document.createElement('span');
|
||||
const span = document.createElement("span");
|
||||
span.innerHTML = innerHTML;
|
||||
return span;
|
||||
},
|
||||
@@ -192,11 +192,11 @@ export function getLanguageExtension({
|
||||
];
|
||||
}
|
||||
|
||||
if (language === 'json') {
|
||||
if (language === "json") {
|
||||
extraExtensions.push(lintExtension ?? linter(jsonParseLinter()));
|
||||
extraExtensions.push(
|
||||
jsoncLanguage.data.of({
|
||||
commentTokens: { line: '//', block: { open: '/*', close: '*/' } },
|
||||
commentTokens: { line: "//", block: { open: "/*", close: "*/" } },
|
||||
}),
|
||||
);
|
||||
if (!hideGutter) {
|
||||
@@ -205,7 +205,7 @@ export function getLanguageExtension({
|
||||
}
|
||||
|
||||
const maybeBase = language ? syntaxExtensions[language] : null;
|
||||
const base = typeof maybeBase === 'function' ? maybeBase() : null;
|
||||
const base = typeof maybeBase === "function" ? maybeBase() : null;
|
||||
if (base == null) {
|
||||
return [];
|
||||
}
|
||||
@@ -229,10 +229,10 @@ export function getLanguageExtension({
|
||||
// Filter out autocomplete start triggers from completionKeymap since we handle it via configurable hotkeys.
|
||||
// Keep navigation keys (ArrowUp/Down, Enter, Escape, etc.) but remove startCompletion bindings.
|
||||
const filteredCompletionKeymap = completionKeymap.filter((binding) => {
|
||||
const key = binding.key?.toLowerCase() ?? '';
|
||||
const mac = (binding as { mac?: string }).mac?.toLowerCase() ?? '';
|
||||
const key = binding.key?.toLowerCase() ?? "";
|
||||
const mac = (binding as { mac?: string }).mac?.toLowerCase() ?? "";
|
||||
// Filter out Ctrl-Space and Mac-specific autocomplete triggers (Alt-`, Alt-i)
|
||||
const isStartTrigger = key.includes('space') || mac.includes('alt-') || mac.includes('`');
|
||||
const isStartTrigger = key.includes("space") || mac.includes("alt-") || mac.includes("`");
|
||||
return !isStartTrigger;
|
||||
});
|
||||
|
||||
@@ -242,7 +242,7 @@ export const baseExtensions = [
|
||||
dropCursor(),
|
||||
drawSelection(),
|
||||
autocompletion({
|
||||
tooltipClass: () => 'x-theme-menu',
|
||||
tooltipClass: () => "x-theme-menu",
|
||||
closeOnBlur: true, // Set to `false` for debugging in devtools without closing it
|
||||
defaultKeymap: false, // We handle the trigger via configurable hotkeys
|
||||
compareCompletions: (a, b) => {
|
||||
@@ -257,7 +257,7 @@ export const baseExtensions = [
|
||||
|
||||
export const readonlyExtensions = [
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.contentAttributes.of({ tabindex: '-1' }),
|
||||
EditorView.contentAttributes.of({ tabindex: "-1" }),
|
||||
];
|
||||
|
||||
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
|
||||
@@ -269,11 +269,11 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
lineNumbers(),
|
||||
foldGutter({
|
||||
markerDOM: (open) => {
|
||||
const el = document.createElement('div');
|
||||
el.classList.add('fold-gutter-icon');
|
||||
const el = document.createElement("div");
|
||||
el.classList.add("fold-gutter-icon");
|
||||
el.tabIndex = -1;
|
||||
if (open) {
|
||||
el.setAttribute('data-open', '');
|
||||
el.setAttribute("data-open", "");
|
||||
}
|
||||
return el;
|
||||
},
|
||||
@@ -281,12 +281,12 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
],
|
||||
codeFolding({
|
||||
placeholderDOM(_view, onclick, prepared) {
|
||||
const el = document.createElement('span');
|
||||
const el = document.createElement("span");
|
||||
el.onclick = onclick;
|
||||
el.className = 'cm-foldPlaceholder';
|
||||
el.innerText = prepared || '…';
|
||||
el.title = 'unfold';
|
||||
el.ariaLabel = 'folded code';
|
||||
el.className = "cm-foldPlaceholder";
|
||||
el.innerText = prepared || "…";
|
||||
el.title = "unfold";
|
||||
el.ariaLabel = "folded code";
|
||||
return el;
|
||||
},
|
||||
/**
|
||||
@@ -295,15 +295,15 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
*/
|
||||
preparePlaceholder(state, range) {
|
||||
let count: number | undefined;
|
||||
let startToken = '{';
|
||||
let endToken = '}';
|
||||
let startToken = "{";
|
||||
let endToken = "}";
|
||||
|
||||
const prevLine = state.doc.lineAt(range.from).text;
|
||||
const isArray = prevLine.lastIndexOf('[') > prevLine.lastIndexOf('{');
|
||||
const isArray = prevLine.lastIndexOf("[") > prevLine.lastIndexOf("{");
|
||||
|
||||
if (isArray) {
|
||||
startToken = '[';
|
||||
endToken = ']';
|
||||
startToken = "[";
|
||||
endToken = "]";
|
||||
}
|
||||
|
||||
const internal = state.sliceDoc(range.from, range.to);
|
||||
@@ -317,7 +317,7 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
}
|
||||
|
||||
if (count !== undefined) {
|
||||
const label = isArray ? 'item' : 'key';
|
||||
const label = isArray ? "item" : "key";
|
||||
return pluralizeCount(label, count);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import { autocompletion, startCompletion } from '@codemirror/autocomplete';
|
||||
import { LanguageSupport, LRLanguage, syntaxTree } from '@codemirror/language';
|
||||
import type { SyntaxNode } from '@lezer/common';
|
||||
import { parser } from './filter';
|
||||
import type { Completion, CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||
import { autocompletion, startCompletion } from "@codemirror/autocomplete";
|
||||
import { LanguageSupport, LRLanguage, syntaxTree } from "@codemirror/language";
|
||||
import type { SyntaxNode } from "@lezer/common";
|
||||
import { parser } from "./filter";
|
||||
|
||||
export interface FieldDef {
|
||||
name: string;
|
||||
@@ -43,7 +43,7 @@ function inPhrase(ctx: CompletionContext): boolean {
|
||||
// Lezer node names from your grammar: Phrase is the quoted token
|
||||
let n: SyntaxNode | null = syntaxTree(ctx.state).resolveInner(ctx.pos, -1);
|
||||
while (n) {
|
||||
if (n.name === 'Phrase') return true;
|
||||
if (n.name === "Phrase") return true;
|
||||
n = n.parent;
|
||||
}
|
||||
return false;
|
||||
@@ -53,7 +53,7 @@ function inPhrase(ctx: CompletionContext): boolean {
|
||||
function inUnclosedQuote(doc: string, pos: number): boolean {
|
||||
let quotes = 0;
|
||||
for (let i = 0; i < pos; i++) {
|
||||
if (doc[i] === '"' && doc[i - 1] !== '\\') quotes++;
|
||||
if (doc[i] === '"' && doc[i - 1] !== "\\") quotes++;
|
||||
}
|
||||
return quotes % 2 === 1; // odd = inside an open quote
|
||||
}
|
||||
@@ -64,13 +64,13 @@ function inUnclosedQuote(doc: string, pos: number): boolean {
|
||||
* - Otherwise, we're in a field name or bare term position.
|
||||
*/
|
||||
function contextInfo(stateDoc: string, pos: number) {
|
||||
const lastColon = stateDoc.lastIndexOf(':', pos - 1);
|
||||
const lastColon = stateDoc.lastIndexOf(":", pos - 1);
|
||||
const lastBoundary = Math.max(
|
||||
stateDoc.lastIndexOf(' ', pos - 1),
|
||||
stateDoc.lastIndexOf('\t', pos - 1),
|
||||
stateDoc.lastIndexOf('\n', pos - 1),
|
||||
stateDoc.lastIndexOf('(', pos - 1),
|
||||
stateDoc.lastIndexOf(')', pos - 1),
|
||||
stateDoc.lastIndexOf(" ", pos - 1),
|
||||
stateDoc.lastIndexOf("\t", pos - 1),
|
||||
stateDoc.lastIndexOf("\n", pos - 1),
|
||||
stateDoc.lastIndexOf("(", pos - 1),
|
||||
stateDoc.lastIndexOf(")", pos - 1),
|
||||
);
|
||||
|
||||
const inValue = lastColon > lastBoundary;
|
||||
@@ -96,7 +96,7 @@ function contextInfo(stateDoc: string, pos: number) {
|
||||
function fieldNameCompletions(fieldNames: string[]): Completion[] {
|
||||
return fieldNames.map((name) => ({
|
||||
label: name,
|
||||
type: 'property',
|
||||
type: "property",
|
||||
apply: (view, _completion, from, to) => {
|
||||
// Insert "name:" (leave cursor right after colon)
|
||||
view.dispatch({
|
||||
@@ -117,7 +117,7 @@ function fieldValueCompletions(
|
||||
return vals.map((v) => ({
|
||||
label: v.match(IDENT_ONLY) ? v : `"${v}"`,
|
||||
displayLabel: v,
|
||||
type: 'constant',
|
||||
type: "constant",
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ function makeCompletionSource(opts: FilterOptions) {
|
||||
}
|
||||
|
||||
const language = LRLanguage.define({
|
||||
name: 'filter',
|
||||
name: "filter",
|
||||
parser,
|
||||
languageData: {
|
||||
autocompletion: {},
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/* oxlint-disable */
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"%QOVQPOOPeOPOOOVQPO'#CfOjQPO'#ChO!XQPO'#CgOOQO'#Cc'#CcOVQPO'#CaOOQO'#Ca'#CaO!oQPO'#C`O!|QPO'#C_OOQO'#C^'#C^QOQPOOPOOO'#Cp'#CpP#XOPO)C>jO#`QPO,59QO#eQPO,59ROOQO,58{,58{OVQPO'#CqOOQO'#Cq'#CqO#mQPO,58zOVQPO'#CrO#zQPO,58yPOOO-E6n-E6nOOQO1G.l1G.lOOQO'#Cm'#CmOOQO'#Ck'#CkOOQO1G.m1G.mOOQO,59],59]OOQO-E6o-E6oOOQO,59^,59^OOQO-E6p-E6p",
|
||||
stateData:
|
||||
'$]~OiPQ~OUUOXQO]RO`TO~Oi[O~OUaXXaX]aX^[X`aXbaXcaXgaXWaX~O^_O~OUUOXQO]RO`TObaO~OcSXgSXWSX~P!^OcdOgRXWRX~Oi[O~Qh]WgO~O]hO`iO~OcSagSaWSa~P!^OcdOgRaWRa~OUbc]c~',
|
||||
goto: '#hgPPhnryP!YPP!c!c!lPP!uP!xPP#U#[#bQZOR^QTYOQSXOQRmdUWOQdQ`USbWcRka_VOQUWacd_TOQUWacd_SOQUWacdRj_^TOQUWacdRi_Q]PRf]QcWRlcQeXRne',
|
||||
"$]~OiPQ~OUUOXQO]RO`TO~Oi[O~OUaXXaX]aX^[X`aXbaXcaXgaXWaX~O^_O~OUUOXQO]RO`TObaO~OcSXgSXWSX~P!^OcdOgRXWRX~Oi[O~Qh]WgO~O]hO`iO~OcSagSaWSa~P!^OcdOgRaWRa~OUbc]c~",
|
||||
goto: "#hgPPhnryP!YPP!c!c!lPP!uP!xPP#U#[#bQZOR^QTYOQSXOQRmdUWOQdQ`USbWcRka_VOQUWacd_TOQUWacd_SOQUWacdRj_^TOQUWacdRi_Q]PRf]QcWRlcQeXRne",
|
||||
nodeNames:
|
||||
'⚠ Query Expr OrExpr AndExpr Unary Not Primary RParen LParen Group Field FieldName Word Colon FieldValue Phrase Term And Or',
|
||||
"⚠ Query Expr OrExpr AndExpr Unary Not Primary RParen LParen Group Field FieldName Word Colon FieldValue Phrase Term And Or",
|
||||
maxTerm: 25,
|
||||
nodeProps: [
|
||||
['openedBy', 8, 'LParen'],
|
||||
['closedBy', 9, 'RParen'],
|
||||
["openedBy", 8, "LParen"],
|
||||
["closedBy", 9, "RParen"],
|
||||
],
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0, 20],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
// Boolean operators
|
||||
@@ -16,6 +16,6 @@ export const highlight = styleTags({
|
||||
Phrase: t.string, // "quoted string"
|
||||
|
||||
// Fields
|
||||
'FieldName/Word': t.attributeName,
|
||||
'FieldValue/Term/Word': t.attributeValue,
|
||||
"FieldName/Word": t.attributeName,
|
||||
"FieldValue/Term/Word": t.attributeValue,
|
||||
});
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
// query.ts
|
||||
// A tiny query language parser with NOT/AND/OR, parentheses, phrases, negation, and field:value.
|
||||
|
||||
import { fuzzyMatch } from 'fuzzbunny';
|
||||
import { fuzzyMatch } from "fuzzbunny";
|
||||
/////////////////////////
|
||||
// AST
|
||||
/////////////////////////
|
||||
|
||||
export type Ast =
|
||||
| { type: 'Term'; value: string } // foo
|
||||
| { type: 'Phrase'; value: string } // "hi there"
|
||||
| { type: 'Field'; field: string; value: string } // method:POST or title:"exact phrase"
|
||||
| { type: 'Not'; node: Ast } // -foo or NOT foo
|
||||
| { type: 'And'; left: Ast; right: Ast } // a AND b
|
||||
| { type: 'Or'; left: Ast; right: Ast }; // a OR b
|
||||
| { type: "Term"; value: string } // foo
|
||||
| { type: "Phrase"; value: string } // "hi there"
|
||||
| { type: "Field"; field: string; value: string } // method:POST or title:"exact phrase"
|
||||
| { type: "Not"; node: Ast } // -foo or NOT foo
|
||||
| { type: "And"; left: Ast; right: Ast } // a AND b
|
||||
| { type: "Or"; left: Ast; right: Ast }; // a OR b
|
||||
|
||||
/////////////////////////
|
||||
// Tokenizer
|
||||
/////////////////////////
|
||||
type Tok =
|
||||
| { kind: 'LPAREN' }
|
||||
| { kind: 'RPAREN' }
|
||||
| { kind: 'AND' }
|
||||
| { kind: 'OR' }
|
||||
| { kind: 'NOT' } // explicit NOT
|
||||
| { kind: 'MINUS' } // unary minus before term/phrase/paren group
|
||||
| { kind: 'COLON' }
|
||||
| { kind: 'WORD'; text: string } // bareword (unquoted)
|
||||
| { kind: 'PHRASE'; text: string } // "quoted phrase"
|
||||
| { kind: 'EOF' };
|
||||
| { kind: "LPAREN" }
|
||||
| { kind: "RPAREN" }
|
||||
| { kind: "AND" }
|
||||
| { kind: "OR" }
|
||||
| { kind: "NOT" } // explicit NOT
|
||||
| { kind: "MINUS" } // unary minus before term/phrase/paren group
|
||||
| { kind: "COLON" }
|
||||
| { kind: "WORD"; text: string } // bareword (unquoted)
|
||||
| { kind: "PHRASE"; text: string } // "quoted phrase"
|
||||
| { kind: "EOF" };
|
||||
|
||||
const isSpace = (c: string) => /\s/.test(c);
|
||||
const isIdent = (c: string) => /[A-Za-z0-9_\-./]/.test(c);
|
||||
@@ -37,11 +37,11 @@ export function tokenize(input: string): Tok[] {
|
||||
let i = 0;
|
||||
const n = input.length;
|
||||
|
||||
const peek = () => input[i] ?? '';
|
||||
const peek = () => input[i] ?? "";
|
||||
const advance = () => input[i++];
|
||||
|
||||
const readWord = () => {
|
||||
let s = '';
|
||||
let s = "";
|
||||
while (i < n && isIdent(peek())) s += advance();
|
||||
return s;
|
||||
};
|
||||
@@ -49,11 +49,11 @@ export function tokenize(input: string): Tok[] {
|
||||
const readPhrase = () => {
|
||||
// assumes current char is opening quote
|
||||
advance(); // consume opening "
|
||||
let s = '';
|
||||
let s = "";
|
||||
while (i < n) {
|
||||
const c = advance();
|
||||
if (c === `"`) break;
|
||||
if (c === '\\' && i < n) {
|
||||
if (c === "\\" && i < n) {
|
||||
// escape \" and \\ (simple)
|
||||
const next = advance();
|
||||
s += next;
|
||||
@@ -72,28 +72,28 @@ export function tokenize(input: string): Tok[] {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c === '(') {
|
||||
toks.push({ kind: 'LPAREN' });
|
||||
if (c === "(") {
|
||||
toks.push({ kind: "LPAREN" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === ')') {
|
||||
toks.push({ kind: 'RPAREN' });
|
||||
if (c === ")") {
|
||||
toks.push({ kind: "RPAREN" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === ':') {
|
||||
toks.push({ kind: 'COLON' });
|
||||
if (c === ":") {
|
||||
toks.push({ kind: "COLON" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === `"`) {
|
||||
const text = readPhrase();
|
||||
toks.push({ kind: 'PHRASE', text });
|
||||
toks.push({ kind: "PHRASE", text });
|
||||
continue;
|
||||
}
|
||||
if (c === '-') {
|
||||
toks.push({ kind: 'MINUS' });
|
||||
if (c === "-") {
|
||||
toks.push({ kind: "MINUS" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
@@ -102,10 +102,10 @@ export function tokenize(input: string): Tok[] {
|
||||
if (isIdent(c)) {
|
||||
const w = readWord();
|
||||
const upper = w.toUpperCase();
|
||||
if (upper === 'AND') toks.push({ kind: 'AND' });
|
||||
else if (upper === 'OR') toks.push({ kind: 'OR' });
|
||||
else if (upper === 'NOT') toks.push({ kind: 'NOT' });
|
||||
else toks.push({ kind: 'WORD', text: w });
|
||||
if (upper === "AND") toks.push({ kind: "AND" });
|
||||
else if (upper === "OR") toks.push({ kind: "OR" });
|
||||
else if (upper === "NOT") toks.push({ kind: "NOT" });
|
||||
else toks.push({ kind: "WORD", text: w });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export function tokenize(input: string): Tok[] {
|
||||
i++;
|
||||
}
|
||||
|
||||
toks.push({ kind: 'EOF' });
|
||||
toks.push({ kind: "EOF" });
|
||||
return toks;
|
||||
}
|
||||
|
||||
@@ -122,20 +122,20 @@ class Parser {
|
||||
constructor(private toks: Tok[]) {}
|
||||
|
||||
private peek(): Tok {
|
||||
return this.toks[this.i] ?? { kind: 'EOF' };
|
||||
return this.toks[this.i] ?? { kind: "EOF" };
|
||||
}
|
||||
private advance(): Tok {
|
||||
return this.toks[this.i++] ?? { kind: 'EOF' };
|
||||
return this.toks[this.i++] ?? { kind: "EOF" };
|
||||
}
|
||||
private at(kind: Tok['kind']) {
|
||||
private at(kind: Tok["kind"]) {
|
||||
return this.peek().kind === kind;
|
||||
}
|
||||
|
||||
// Top-level: parse OR-precedence chain, allowing implicit AND.
|
||||
parse(): Ast | null {
|
||||
if (this.at('EOF')) return null;
|
||||
if (this.at("EOF")) return null;
|
||||
const expr = this.parseOr();
|
||||
if (!this.at('EOF')) {
|
||||
if (!this.at("EOF")) {
|
||||
// Optionally, consume remaining tokens or throw
|
||||
}
|
||||
return expr;
|
||||
@@ -144,10 +144,10 @@ class Parser {
|
||||
// Precedence: NOT (highest), AND, OR (lowest)
|
||||
private parseOr(): Ast {
|
||||
let node = this.parseAnd();
|
||||
while (this.at('OR')) {
|
||||
while (this.at("OR")) {
|
||||
this.advance();
|
||||
const rhs = this.parseAnd();
|
||||
node = { type: 'Or', left: node, right: rhs };
|
||||
node = { type: "Or", left: node, right: rhs };
|
||||
}
|
||||
return node;
|
||||
}
|
||||
@@ -155,31 +155,31 @@ class Parser {
|
||||
private parseAnd(): Ast {
|
||||
let node = this.parseUnary();
|
||||
// Implicit AND: if next token starts a primary, treat as AND.
|
||||
while (this.at('AND') || this.startsPrimary()) {
|
||||
if (this.at('AND')) this.advance();
|
||||
while (this.at("AND") || this.startsPrimary()) {
|
||||
if (this.at("AND")) this.advance();
|
||||
const rhs = this.parseUnary();
|
||||
node = { type: 'And', left: node, right: rhs };
|
||||
node = { type: "And", left: node, right: rhs };
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private parseUnary(): Ast {
|
||||
if (this.at('NOT') || this.at('MINUS')) {
|
||||
if (this.at("NOT") || this.at("MINUS")) {
|
||||
this.advance();
|
||||
const node = this.parseUnary();
|
||||
return { type: 'Not', node };
|
||||
return { type: "Not", node };
|
||||
}
|
||||
return this.parsePrimaryOrField();
|
||||
}
|
||||
|
||||
private startsPrimary(): boolean {
|
||||
const k = this.peek().kind;
|
||||
return k === 'WORD' || k === 'PHRASE' || k === 'LPAREN' || k === 'MINUS' || k === 'NOT';
|
||||
return k === "WORD" || k === "PHRASE" || k === "LPAREN" || k === "MINUS" || k === "NOT";
|
||||
}
|
||||
|
||||
private parsePrimaryOrField(): Ast {
|
||||
// Parenthesized group
|
||||
if (this.at('LPAREN')) {
|
||||
if (this.at("LPAREN")) {
|
||||
this.advance();
|
||||
const inside = this.parseOr();
|
||||
// if (!this.at('RPAREN')) throw new Error("Missing closing ')'");
|
||||
@@ -188,59 +188,59 @@ class Parser {
|
||||
}
|
||||
|
||||
// Phrase
|
||||
if (this.at('PHRASE')) {
|
||||
const t = this.advance() as Extract<Tok, { kind: 'PHRASE' }>;
|
||||
return { type: 'Phrase', value: t.text };
|
||||
if (this.at("PHRASE")) {
|
||||
const t = this.advance() as Extract<Tok, { kind: "PHRASE" }>;
|
||||
return { type: "Phrase", value: t.text };
|
||||
}
|
||||
|
||||
// Field or bare word
|
||||
if (this.at('WORD')) {
|
||||
const wordTok = this.advance() as Extract<Tok, { kind: 'WORD' }>;
|
||||
if (this.at("WORD")) {
|
||||
const wordTok = this.advance() as Extract<Tok, { kind: "WORD" }>;
|
||||
|
||||
if (this.at('COLON')) {
|
||||
if (this.at("COLON")) {
|
||||
// field:value or field:"phrase"
|
||||
this.advance(); // :
|
||||
let value: string;
|
||||
if (this.at('PHRASE')) {
|
||||
const p = this.advance() as Extract<Tok, { kind: 'PHRASE' }>;
|
||||
if (this.at("PHRASE")) {
|
||||
const p = this.advance() as Extract<Tok, { kind: "PHRASE" }>;
|
||||
value = p.text;
|
||||
} else if (this.at('WORD')) {
|
||||
const w = this.advance() as Extract<Tok, { kind: 'WORD' }>;
|
||||
} else if (this.at("WORD")) {
|
||||
const w = this.advance() as Extract<Tok, { kind: "WORD" }>;
|
||||
value = w.text;
|
||||
} else {
|
||||
// Anything else after colon is treated literally as a single Term token.
|
||||
const t = this.advance();
|
||||
value = tokText(t);
|
||||
}
|
||||
return { type: 'Field', field: wordTok.text, value };
|
||||
return { type: "Field", field: wordTok.text, value };
|
||||
}
|
||||
|
||||
// plain term
|
||||
return { type: 'Term', value: wordTok.text };
|
||||
return { type: "Term", value: wordTok.text };
|
||||
}
|
||||
|
||||
const w = this.advance() as Extract<Tok, { kind: 'WORD' }>;
|
||||
return { type: 'Phrase', value: 'text' in w ? w.text : '' };
|
||||
const w = this.advance() as Extract<Tok, { kind: "WORD" }>;
|
||||
return { type: "Phrase", value: "text" in w ? w.text : "" };
|
||||
}
|
||||
}
|
||||
|
||||
function tokText(t: Tok): string {
|
||||
if ('text' in t) return t.text;
|
||||
if ("text" in t) return t.text;
|
||||
|
||||
switch (t.kind) {
|
||||
case 'COLON':
|
||||
return ':';
|
||||
case 'LPAREN':
|
||||
return '(';
|
||||
case 'RPAREN':
|
||||
return ')';
|
||||
case "COLON":
|
||||
return ":";
|
||||
case "LPAREN":
|
||||
return "(";
|
||||
case "RPAREN":
|
||||
return ")";
|
||||
default:
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function parseQuery(q: string): Ast | null {
|
||||
if (q.trim() === '') return null;
|
||||
if (q.trim() === "") return null;
|
||||
const toks = tokenize(q);
|
||||
const parser = new Parser(toks);
|
||||
return parser.parse();
|
||||
@@ -251,45 +251,45 @@ export type Doc = {
|
||||
fields?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type Technique = 'substring' | 'fuzzy' | 'strict';
|
||||
type Technique = "substring" | "fuzzy" | "strict";
|
||||
|
||||
function includes(hay: string | undefined, needle: string, technique: Technique): boolean {
|
||||
if (!hay || !needle) return false;
|
||||
if (technique === 'strict') return hay === needle;
|
||||
if (technique === 'fuzzy') return !!fuzzyMatch(hay, needle);
|
||||
if (technique === "strict") return hay === needle;
|
||||
if (technique === "fuzzy") return !!fuzzyMatch(hay, needle);
|
||||
return hay.indexOf(needle) !== -1;
|
||||
}
|
||||
|
||||
export function evaluate(ast: Ast | null, doc: Doc): boolean {
|
||||
if (!ast) return true; // Match everything if no query is provided
|
||||
|
||||
const text = (doc.text ?? '').toLowerCase();
|
||||
const text = (doc.text ?? "").toLowerCase();
|
||||
const fieldsNorm: Record<string, string[]> = {};
|
||||
|
||||
for (const [k, v] of Object.entries(doc.fields ?? {})) {
|
||||
if (!(typeof v === 'string' || Array.isArray(v))) continue;
|
||||
if (!(typeof v === "string" || Array.isArray(v))) continue;
|
||||
fieldsNorm[k.toLowerCase()] = Array.isArray(v)
|
||||
? v.filter((v) => typeof v === 'string').map((s) => s.toLowerCase())
|
||||
: [String(v ?? '').toLowerCase()];
|
||||
? v.filter((v) => typeof v === "string").map((s) => s.toLowerCase())
|
||||
: [String(v ?? "").toLowerCase()];
|
||||
}
|
||||
|
||||
const evalNode = (node: Ast): boolean => {
|
||||
switch (node.type) {
|
||||
case 'Term':
|
||||
return includes(text, node.value.toLowerCase(), 'fuzzy');
|
||||
case 'Phrase':
|
||||
case "Term":
|
||||
return includes(text, node.value.toLowerCase(), "fuzzy");
|
||||
case "Phrase":
|
||||
// Quoted phrases match exactly
|
||||
return includes(text, node.value.toLowerCase(), 'substring');
|
||||
case 'Field': {
|
||||
return includes(text, node.value.toLowerCase(), "substring");
|
||||
case "Field": {
|
||||
const vals = fieldsNorm[node.field.toLowerCase()] ?? [];
|
||||
if (vals.length === 0) return false;
|
||||
return vals.some((v) => includes(v, node.value.toLowerCase(), 'substring'));
|
||||
return vals.some((v) => includes(v, node.value.toLowerCase(), "substring"));
|
||||
}
|
||||
case 'Not':
|
||||
case "Not":
|
||||
return !evalNode(node.node);
|
||||
case 'And':
|
||||
case "And":
|
||||
return evalNode(node.left) && evalNode(node.right);
|
||||
case 'Or':
|
||||
case "Or":
|
||||
return evalNode(node.left) || evalNode(node.right);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CompletionContext } from '@codemirror/autocomplete';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import { defaultBoost } from './twig/completion';
|
||||
import type { CompletionContext } from "@codemirror/autocomplete";
|
||||
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
||||
import { defaultBoost } from "./twig/completion";
|
||||
|
||||
export interface GenericCompletionConfig {
|
||||
minMatch?: number;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, EditorView, hoverTooltip, MatchDecorator, ViewPlugin } from '@codemirror/view';
|
||||
import { activeWorkspaceIdAtom } from '../../../../hooks/useActiveWorkspace';
|
||||
import { copyToClipboard } from '../../../../lib/copy';
|
||||
import { createRequestAndNavigate } from '../../../../lib/createRequestAndNavigate';
|
||||
import { jotaiStore } from '../../../../lib/jotai';
|
||||
import type { DecorationSet, ViewUpdate } from "@codemirror/view";
|
||||
import { Decoration, EditorView, hoverTooltip, MatchDecorator, ViewPlugin } from "@codemirror/view";
|
||||
import { activeWorkspaceIdAtom } from "../../../../hooks/useActiveWorkspace";
|
||||
import { copyToClipboard } from "../../../../lib/copy";
|
||||
import { createRequestAndNavigate } from "../../../../lib/createRequestAndNavigate";
|
||||
import { jotaiStore } from "../../../../lib/jotai";
|
||||
|
||||
const REGEX =
|
||||
/(https?:\/\/([-a-zA-Z0-9@:%._+*~#=]{1,256})+(\.[a-zA-Z0-9()]{1,6})?\b([-a-zA-Z0-9()@:%_+*.~#?&/={}[\]]*))/g;
|
||||
@@ -39,26 +39,26 @@ const tooltip = hoverTooltip(
|
||||
create() {
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
const link = text.substring(found?.start - from, found?.end - from);
|
||||
const dom = document.createElement('div');
|
||||
const dom = document.createElement("div");
|
||||
|
||||
const $open = document.createElement('a');
|
||||
$open.textContent = 'Open in browser';
|
||||
const $open = document.createElement("a");
|
||||
$open.textContent = "Open in browser";
|
||||
$open.href = link;
|
||||
$open.target = '_blank';
|
||||
$open.rel = 'noopener noreferrer';
|
||||
$open.target = "_blank";
|
||||
$open.rel = "noopener noreferrer";
|
||||
|
||||
const $copy = document.createElement('button');
|
||||
$copy.textContent = 'Copy to clipboard';
|
||||
$copy.addEventListener('click', () => {
|
||||
const $copy = document.createElement("button");
|
||||
$copy.textContent = "Copy to clipboard";
|
||||
$copy.addEventListener("click", () => {
|
||||
copyToClipboard(link);
|
||||
});
|
||||
|
||||
const $create = document.createElement('button');
|
||||
$create.textContent = 'Create new request';
|
||||
$create.addEventListener('click', async () => {
|
||||
const $create = document.createElement("button");
|
||||
$create.textContent = "Create new request";
|
||||
$create.addEventListener("click", async () => {
|
||||
await createRequestAndNavigate({
|
||||
model: 'http_request',
|
||||
workspaceId: workspaceId ?? 'n/a',
|
||||
model: "http_request",
|
||||
workspaceId: workspaceId ?? "n/a",
|
||||
url: link,
|
||||
});
|
||||
});
|
||||
@@ -94,12 +94,12 @@ const decorator = () => {
|
||||
const groupMatch = match[1];
|
||||
if (groupMatch == null) {
|
||||
// Should never happen, but make TS happy
|
||||
console.warn('Group match was empty', match);
|
||||
console.warn("Group match was empty", match);
|
||||
return Decoration.replace({});
|
||||
}
|
||||
|
||||
return Decoration.mark({
|
||||
class: 'hyperlink-widget',
|
||||
class: "hyperlink-widget",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Diagnostic } from '@codemirror/lint';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import { parse as jsonLintParse } from '@prantlf/jsonlint';
|
||||
import type { Diagnostic } from "@codemirror/lint";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { parse as jsonLintParse } from "@prantlf/jsonlint";
|
||||
|
||||
const TEMPLATE_SYNTAX_REGEX = /\$\{\[[\s\S]*?]}/g;
|
||||
|
||||
@@ -15,14 +15,14 @@ export function jsonParseLinter(options?: JsonLintOptions) {
|
||||
const doc = view.state.doc.toString();
|
||||
// We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template
|
||||
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
|
||||
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => '1'.repeat(m.length));
|
||||
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => "1".repeat(m.length));
|
||||
jsonLintParse(escapedDoc, {
|
||||
mode: (options?.allowComments ?? true) ? 'cjson' : 'json',
|
||||
mode: (options?.allowComments ?? true) ? "cjson" : "json",
|
||||
ignoreTrailingCommas: options?.allowTrailingCommas ?? false,
|
||||
});
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
} catch (err: any) {
|
||||
if (!('location' in err)) {
|
||||
if (!("location" in err)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function jsonParseLinter(options?: JsonLintOptions) {
|
||||
{
|
||||
from: err.location.start.offset,
|
||||
to: err.location.start.offset,
|
||||
severity: 'error',
|
||||
severity: "error",
|
||||
message: err.message,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parser } from './pairs';
|
||||
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
||||
import { parser } from "./pairs";
|
||||
|
||||
const language = LRLanguage.define({
|
||||
name: 'pairs',
|
||||
name: "pairs",
|
||||
parser,
|
||||
languageData: {},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
Sep: t.bracket,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "zQQOPOOOVOQO'#CaQQOPOOO[OSO,58{OOOO-E6_-E6_OaOQO1G.gOOOO7+$R7+$R",
|
||||
stateData: 'f~OQPO~ORRO~OSTO~OVUO~O',
|
||||
goto: ']UPPPPPVQQORSQ',
|
||||
nodeNames: '⚠ pairs Key Sep Value',
|
||||
stateData: "f~OQPO~ORRO~OSTO~OVUO~O",
|
||||
goto: "]UPPPPPVQQORSQ",
|
||||
nodeNames: "⚠ pairs Key Sep Value",
|
||||
maxTerm: 7,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
@@ -17,13 +17,13 @@ export const parser = LRParser.deserialize({
|
||||
topRules: { pairs: [0, 1] },
|
||||
tokenPrec: 0,
|
||||
termNames: {
|
||||
'0': '⚠',
|
||||
'1': '@top',
|
||||
'2': 'Key',
|
||||
'3': 'Sep',
|
||||
'4': 'Value',
|
||||
'5': '(Key Sep Value "\\n")+',
|
||||
'6': '␄',
|
||||
'7': '"\\n"',
|
||||
"0": "⚠",
|
||||
"1": "@top",
|
||||
"2": "Key",
|
||||
"3": "Sep",
|
||||
"4": "Value",
|
||||
"5": '(Key Sep Value "\\n")+',
|
||||
"6": "␄",
|
||||
"7": '"\\n"',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getSearchQuery, searchPanelOpen } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
||||
import { getSearchQuery, searchPanelOpen } from "@codemirror/search";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { type EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
|
||||
|
||||
/**
|
||||
* A CodeMirror extension that displays the total number of search matches
|
||||
@@ -41,7 +41,7 @@ export function searchMatchCount(): Extension {
|
||||
|
||||
if (!query.search) {
|
||||
if (this.countEl) {
|
||||
this.countEl.textContent = '0/0';
|
||||
this.countEl.textContent = "0/0";
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export function searchMatchCount(): Extension {
|
||||
if (count > MAX_COUNT) {
|
||||
this.countEl.textContent = `${MAX_COUNT}+`;
|
||||
} else if (count === 0) {
|
||||
this.countEl.textContent = '0/0';
|
||||
this.countEl.textContent = "0/0";
|
||||
} else if (currentIndex > 0) {
|
||||
this.countEl.textContent = `${currentIndex}/${count}`;
|
||||
} else {
|
||||
@@ -75,7 +75,7 @@ export function searchMatchCount(): Extension {
|
||||
|
||||
private ensureCountEl() {
|
||||
// Find the search panel in the editor DOM
|
||||
const panel = this.view.dom.querySelector('.cm-search');
|
||||
const panel = this.view.dom.querySelector(".cm-search");
|
||||
if (!panel) {
|
||||
this.countEl = null;
|
||||
return;
|
||||
@@ -85,11 +85,11 @@ export function searchMatchCount(): Extension {
|
||||
return; // Already attached
|
||||
}
|
||||
|
||||
this.countEl = document.createElement('span');
|
||||
this.countEl.className = 'cm-search-match-count';
|
||||
this.countEl = document.createElement("span");
|
||||
this.countEl.className = "cm-search-match-count";
|
||||
|
||||
// Reorder: insert prev button, then next button, then count after the search input
|
||||
const searchInput = panel.querySelector('input');
|
||||
const searchInput = panel.querySelector("input");
|
||||
const prevBtn = panel.querySelector('button[name="prev"]');
|
||||
const nextBtn = panel.querySelector('button[name="next"]');
|
||||
if (searchInput && searchInput.parentElement === panel) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Extension, TransactionSpec } from '@codemirror/state';
|
||||
import { EditorSelection, EditorState, Transaction } from '@codemirror/state';
|
||||
import type { Extension, TransactionSpec } from "@codemirror/state";
|
||||
import { EditorSelection, EditorState, Transaction } from "@codemirror/state";
|
||||
|
||||
/**
|
||||
* A CodeMirror extension that forces single-line input by stripping
|
||||
@@ -17,14 +17,14 @@ import { EditorSelection, EditorState, Transaction } from '@codemirror/state';
|
||||
export function singleLineExtensions(): Extension {
|
||||
return EditorState.transactionFilter.of(
|
||||
(tr: Transaction): TransactionSpec | readonly TransactionSpec[] => {
|
||||
if (!tr.isUserEvent('input') || tr.isUserEvent('input.type.compose')) return tr;
|
||||
if (!tr.isUserEvent("input") || tr.isUserEvent("input.type.compose")) return tr;
|
||||
|
||||
const changes: { from: number; to: number; insert: string }[] = [];
|
||||
|
||||
tr.changes.iterChanges((_fromA, toA, fromB, _toB, inserted) => {
|
||||
let insert = '';
|
||||
let insert = "";
|
||||
for (const line of inserted.iterLines()) {
|
||||
insert += line.replace(/\n/g, '');
|
||||
insert += line.replace(/\n/g, "");
|
||||
}
|
||||
|
||||
if (insert !== inserted.toString()) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parser } from './text';
|
||||
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
||||
import { parser } from "./text";
|
||||
|
||||
export const textLanguage = LRLanguage.define({
|
||||
name: 'text',
|
||||
name: "text",
|
||||
parser,
|
||||
languageData: {},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { LRParser } from "@lezer/lr";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: '[OQOPOOQOOOOO',
|
||||
stateData: 'V~OQPO~O',
|
||||
goto: 'QPP',
|
||||
nodeNames: '⚠ Template Text',
|
||||
states: "[OQOPOOQOOOOO",
|
||||
stateData: "V~OQPO~O",
|
||||
goto: "QPP",
|
||||
nodeNames: "⚠ Template Text",
|
||||
maxTerm: 3,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 0,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parser } from './timeline';
|
||||
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
||||
import { parser } from "./timeline";
|
||||
|
||||
export const timelineLanguage = LRLanguage.define({
|
||||
name: 'timeline',
|
||||
name: "timeline",
|
||||
parser,
|
||||
languageData: {},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
OutgoingText: t.propertyName, // > lines - primary color (matches timeline icons)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
Timeline = 1,
|
||||
export const Timeline = 1,
|
||||
OutgoingLine = 2,
|
||||
OutgoingText = 3,
|
||||
Newline = 4,
|
||||
@@ -9,4 +8,4 @@ export const
|
||||
InfoLine = 7,
|
||||
InfoText = 8,
|
||||
PlainLine = 9,
|
||||
PlainText = 10
|
||||
PlainText = 10;
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {highlight} from "./highlight"
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "!pQQOPOOO`OPO'#C^OeOPO'#CaOjOPO'#CcOoOPO'#CeOOOO'#Ci'#CiOOOO'#Cg'#CgQQOPOOOOOO,58x,58xOOOO,58{,58{OOOO,58},58}OOOO,59P,59POOOO-E6e-E6e",
|
||||
states:
|
||||
"!pQQOPOOO`OPO'#C^OeOPO'#CaOjOPO'#CcOoOPO'#CeOOOO'#Ci'#CiOOOO'#Cg'#CgQQOPOOOOOO,58x,58xOOOO,58{,58{OOOO,58},58}OOOO,59P,59POOOO-E6e-E6e",
|
||||
stateData: "z~ORPOUQOWROYSO~OSWO~OSXO~OSYO~OSZO~ORUWYW~",
|
||||
goto: "m^PP_PP_P_P_PcPiTTOVQVOR[VTUOV",
|
||||
nodeNames: "⚠ Timeline OutgoingLine OutgoingText Newline IncomingLine IncomingText InfoLine InfoText PlainLine PlainText",
|
||||
nodeNames:
|
||||
"⚠ Timeline OutgoingLine OutgoingText Newline IncomingLine IncomingText InfoLine InfoText PlainLine PlainText",
|
||||
maxTerm: 13,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: "%h~RZOYtYZ!]Zztz{!b{!^t!^!_#d!_!`t!`!a$f!a;'St;'S;=`!V<%lOt~ySY~OYtZ;'St;'S;=`!V<%lOt~!YP;=`<%lt~!bOS~~!gUY~OYtZptpq!yq;'St;'S;=`!V<%lOt~#QSW~Y~OY!yZ;'S!y;'S;=`#^<%lO!y~#aP;=`<%l!y~#iUY~OYtZptpq#{q;'St;'S;=`!V<%lOt~$SSU~Y~OY#{Z;'S#{;'S;=`$`<%lO#{~$cP;=`<%l#{~$kUY~OYtZptpq$}q;'St;'S;=`!V<%lOt~%USR~Y~OY$}Z;'S$};'S;=`%b<%lO$}~%eP;=`<%l$}",
|
||||
tokenData:
|
||||
"%h~RZOYtYZ!]Zztz{!b{!^t!^!_#d!_!`t!`!a$f!a;'St;'S;=`!V<%lOt~ySY~OYtZ;'St;'S;=`!V<%lOt~!YP;=`<%lt~!bOS~~!gUY~OYtZptpq!yq;'St;'S;=`!V<%lOt~#QSW~Y~OY!yZ;'S!y;'S;=`#^<%lO!y~#aP;=`<%l!y~#iUY~OYtZptpq#{q;'St;'S;=`!V<%lOt~$SSU~Y~OY#{Z;'S#{;'S;=`$`<%lO#{~$cP;=`<%l#{~$kUY~OYtZptpq$}q;'St;'S;=`!V<%lOt~%USR~Y~OY$}Z;'S$};'S;=`%b<%lO$}~%eP;=`<%l$}",
|
||||
tokenizers: [0],
|
||||
topRules: {"Timeline":[0,1]},
|
||||
tokenPrec: 36
|
||||
})
|
||||
topRules: { Timeline: [0, 1] },
|
||||
tokenPrec: 36,
|
||||
});
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { Completion, CompletionContext } from '@codemirror/autocomplete';
|
||||
import { startCompletion } from '@codemirror/autocomplete';
|
||||
import type { TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import type { Completion, CompletionContext } from "@codemirror/autocomplete";
|
||||
import { startCompletion } from "@codemirror/autocomplete";
|
||||
import type { TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
|
||||
const openTag = '${[ ';
|
||||
const closeTag = ' ]}';
|
||||
const openTag = "${[ ";
|
||||
const closeTag = " ]}";
|
||||
|
||||
export type TwigCompletionOptionVariable = {
|
||||
type: 'variable';
|
||||
type: "variable";
|
||||
};
|
||||
|
||||
export type TwigCompletionOptionNamespace = {
|
||||
type: 'namespace';
|
||||
type: "namespace";
|
||||
};
|
||||
|
||||
export type TwigCompletionOptionFunction = TemplateFunction & {
|
||||
type: 'function';
|
||||
type: "function";
|
||||
};
|
||||
|
||||
export type TwigCompletionOption = (
|
||||
@@ -50,17 +50,17 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
|
||||
const completions: Completion[] = options
|
||||
.flatMap((o): Completion[] => {
|
||||
const matchSegments = toMatch.text.replace(/^\$/, '').split('.');
|
||||
const optionSegments = o.name.split('.');
|
||||
const matchSegments = toMatch.text.replace(/^\$/, "").split(".");
|
||||
const optionSegments = o.name.split(".");
|
||||
|
||||
// If not on the last segment, only complete the namespace
|
||||
if (matchSegments.length < optionSegments.length) {
|
||||
const prefix = optionSegments.slice(0, matchSegments.length).join('.');
|
||||
const prefix = optionSegments.slice(0, matchSegments.length).join(".");
|
||||
return [
|
||||
{
|
||||
label: `${prefix}.*`,
|
||||
type: 'namespace',
|
||||
detail: 'namespace',
|
||||
type: "namespace",
|
||||
detail: "namespace",
|
||||
apply: (view, _completion, from, to) => {
|
||||
const insert = `${prefix}.`;
|
||||
view.dispatch({
|
||||
@@ -75,13 +75,13 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
}
|
||||
|
||||
// If on the last segment, wrap the entire tag
|
||||
const inner = o.type === 'function' ? `${o.name}()` : o.name;
|
||||
const inner = o.type === "function" ? `${o.name}()` : o.name;
|
||||
return [
|
||||
{
|
||||
label: o.name,
|
||||
info: o.description,
|
||||
detail: o.type,
|
||||
type: o.type === 'variable' ? 'variable' : 'function',
|
||||
type: o.type === "variable" ? "variable" : "function",
|
||||
apply: (view, _completion, from, to) => {
|
||||
const insert = openTag + inner + closeTag;
|
||||
view.dispatch({
|
||||
@@ -94,7 +94,7 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
})
|
||||
.filter((v) => v != null);
|
||||
|
||||
const uniqueCompletions = uniqueBy(completions, 'label');
|
||||
const uniqueCompletions = uniqueBy(completions, "label");
|
||||
const sortedCompletions = uniqueCompletions.sort((a, b) => {
|
||||
const boostDiff = defaultBoost(b) - defaultBoost(a);
|
||||
if (boostDiff !== 0) return boostDiff;
|
||||
@@ -119,9 +119,9 @@ export function uniqueBy<T, K extends keyof T>(arr: T[], key: K): T[] {
|
||||
}
|
||||
|
||||
export function defaultBoost(o: Completion) {
|
||||
if (o.type === 'variable') return 4;
|
||||
if (o.type === 'constant') return 3;
|
||||
if (o.type === 'function') return 2;
|
||||
if (o.type === 'namespace') return 1;
|
||||
if (o.type === "variable") return 4;
|
||||
if (o.type === "constant") return 3;
|
||||
if (o.type === "function") return 2;
|
||||
if (o.type === "namespace") return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { LanguageSupport } from '@codemirror/language';
|
||||
import { LRLanguage } from '@codemirror/language';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { parseMixed } from '@lezer/common';
|
||||
import type { WrappedEnvironmentVariable } from '../../../../hooks/useEnvironmentVariables';
|
||||
import type { GenericCompletionConfig } from '../genericCompletion';
|
||||
import { genericCompletion } from '../genericCompletion';
|
||||
import { textLanguage } from '../text/extension';
|
||||
import type { TwigCompletionOption } from './completion';
|
||||
import { twigCompletion } from './completion';
|
||||
import { templateTagsPlugin } from './templateTags';
|
||||
import { parser as twigParser } from './twig';
|
||||
import type { LanguageSupport } from "@codemirror/language";
|
||||
import { LRLanguage } from "@codemirror/language";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { parseMixed } from "@lezer/common";
|
||||
import type { WrappedEnvironmentVariable } from "../../../../hooks/useEnvironmentVariables";
|
||||
import type { GenericCompletionConfig } from "../genericCompletion";
|
||||
import { genericCompletion } from "../genericCompletion";
|
||||
import { textLanguage } from "../text/extension";
|
||||
import type { TwigCompletionOption } from "./completion";
|
||||
import { twigCompletion } from "./completion";
|
||||
import { templateTagsPlugin } from "./templateTags";
|
||||
import { parser as twigParser } from "./twig";
|
||||
|
||||
export function twig({
|
||||
base,
|
||||
@@ -35,7 +35,7 @@ export function twig({
|
||||
environmentVariables.map((v) => ({
|
||||
name: v.variable.name,
|
||||
value: v.variable.value,
|
||||
type: 'variable',
|
||||
type: "variable",
|
||||
label: v.variable.name,
|
||||
description: `Inherited from ${v.source}`,
|
||||
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
|
||||
@@ -74,12 +74,12 @@ function mixLanguage(base: LanguageSupport): LRLanguage {
|
||||
|
||||
return {
|
||||
parser: base.language.parser,
|
||||
overlay: (node) => node.type.name === 'Text',
|
||||
overlay: (node) => node.type.name === "Text",
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const language = LRLanguage.define({ name: 'twig', parser });
|
||||
const language = LRLanguage.define({ name: "twig", parser });
|
||||
mixedLanguagesCache[base.language.name] = language;
|
||||
return language;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
TagOpen: t.bracket,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { Range } from '@codemirror/state';
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import type { Range } from "@codemirror/state";
|
||||
import type { DecorationSet, ViewUpdate } from "@codemirror/view";
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
||||
|
||||
class PathPlaceholderWidget extends WidgetType {
|
||||
readonly #clickListenerCallback: () => void;
|
||||
@@ -22,15 +22,15 @@ class PathPlaceholderWidget extends WidgetType {
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const elt = document.createElement('span');
|
||||
elt.className = 'x-theme-templateTag x-theme-templateTag--secondary template-tag';
|
||||
const elt = document.createElement("span");
|
||||
elt.className = "x-theme-templateTag x-theme-templateTag--secondary template-tag";
|
||||
elt.textContent = this.rawText;
|
||||
elt.addEventListener('click', this.#clickListenerCallback);
|
||||
elt.addEventListener("click", this.#clickListenerCallback);
|
||||
return elt;
|
||||
}
|
||||
|
||||
destroy(dom: HTMLElement) {
|
||||
dom.removeEventListener('click', this.#clickListenerCallback);
|
||||
dom.removeEventListener("click", this.#clickListenerCallback);
|
||||
super.destroy(dom);
|
||||
}
|
||||
|
||||
@@ -50,14 +50,14 @@ function pathParameters(
|
||||
from,
|
||||
to,
|
||||
enter(node) {
|
||||
if (node.name === 'Text') {
|
||||
if (node.name === "Text") {
|
||||
// Find the `url` node and then jump into it to find the placeholders
|
||||
for (let i = node.from; i < node.to; i++) {
|
||||
const innerTree = syntaxTree(view.state).resolveInner(i);
|
||||
if (innerTree.node.name === 'url') {
|
||||
if (innerTree.node.name === "url") {
|
||||
innerTree.toTree().iterate({
|
||||
enter(node) {
|
||||
if (node.name !== 'Placeholder') return;
|
||||
if (node.name !== "Placeholder") return;
|
||||
const globalFrom = innerTree.node.from + node.from;
|
||||
const globalTo = innerTree.node.from + node.to;
|
||||
const rawText = view.state.doc.sliceString(globalFrom, globalTo);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { Range } from '@codemirror/state';
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
import type { SyntaxNodeRef } from '@lezer/common';
|
||||
import { applyFormInputDefaults, validateTemplateFunctionArgs } from '@yaakapp-internal/lib';
|
||||
import type { FormInput, JsonPrimitive, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import { parseTemplate } from '@yaakapp-internal/templates';
|
||||
import type { TwigCompletionOption } from './completion';
|
||||
import { collectArgumentValues } from './util';
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import type { Range } from "@codemirror/state";
|
||||
import type { DecorationSet, ViewUpdate } from "@codemirror/view";
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
||||
import type { SyntaxNodeRef } from "@lezer/common";
|
||||
import { applyFormInputDefaults, validateTemplateFunctionArgs } from "@yaakapp-internal/lib";
|
||||
import type { FormInput, JsonPrimitive, TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
import { parseTemplate } from "@yaakapp-internal/templates";
|
||||
import type { TwigCompletionOption } from "./completion";
|
||||
import { collectArgumentValues } from "./util";
|
||||
|
||||
class TemplateTagWidget extends WidgetType {
|
||||
readonly #clickListenerCallback: () => void;
|
||||
@@ -34,24 +34,24 @@ class TemplateTagWidget extends WidgetType {
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const elt = document.createElement('span');
|
||||
const elt = document.createElement("span");
|
||||
elt.className = `x-theme-templateTag template-tag ${
|
||||
this.option.invalid
|
||||
? 'x-theme-templateTag--danger'
|
||||
: this.option.type === 'variable'
|
||||
? 'x-theme-templateTag--primary'
|
||||
: 'x-theme-templateTag--info'
|
||||
? "x-theme-templateTag--danger"
|
||||
: this.option.type === "variable"
|
||||
? "x-theme-templateTag--primary"
|
||||
: "x-theme-templateTag--info"
|
||||
}`;
|
||||
elt.title = this.option.invalid ? 'Not Found' : (this.option.value ?? '');
|
||||
elt.setAttribute('data-tag-type', this.option.type);
|
||||
if (typeof this.option.label === 'string') elt.textContent = this.option.label;
|
||||
elt.title = this.option.invalid ? "Not Found" : (this.option.value ?? "");
|
||||
elt.setAttribute("data-tag-type", this.option.type);
|
||||
if (typeof this.option.label === "string") elt.textContent = this.option.label;
|
||||
else elt.appendChild(this.option.label);
|
||||
elt.addEventListener('click', this.#clickListenerCallback);
|
||||
elt.addEventListener("click", this.#clickListenerCallback);
|
||||
return elt;
|
||||
}
|
||||
|
||||
destroy(dom: HTMLElement) {
|
||||
dom.removeEventListener('click', this.#clickListenerCallback);
|
||||
dom.removeEventListener("click", this.#clickListenerCallback);
|
||||
super.destroy(dom);
|
||||
}
|
||||
|
||||
@@ -72,34 +72,34 @@ function templateTags(
|
||||
from,
|
||||
to,
|
||||
enter(node) {
|
||||
if (node.name === 'Tag') {
|
||||
if (node.name === "Tag") {
|
||||
// Don't decorate if the cursor is inside the match
|
||||
if (isSelectionInsideNode(view, node)) return;
|
||||
|
||||
const rawTag = view.state.doc.sliceString(node.from, node.to);
|
||||
|
||||
// TODO: Search `node.tree` instead of using Regex here
|
||||
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
|
||||
const inner = rawTag.replace(/^\$\{\[\s*/, "").replace(/\s*]}$/, "");
|
||||
let name = inner.match(/([\w.]+)[(]/)?.[1] ?? inner;
|
||||
|
||||
if (inner.includes('\n')) {
|
||||
if (inner.includes("\n")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The beta named the function `Response` but was changed in stable.
|
||||
// Keep this here for a while because there's no easy way to migrate
|
||||
if (name === 'Response') {
|
||||
name = 'response';
|
||||
if (name === "Response") {
|
||||
name = "response";
|
||||
}
|
||||
|
||||
let option = options.find(
|
||||
(o) => o.name === name || (o.type === 'function' && o.aliases?.includes(name)),
|
||||
(o) => o.name === name || (o.type === "function" && o.aliases?.includes(name)),
|
||||
);
|
||||
|
||||
if (option == null) {
|
||||
const from = node.from; // Cache here so the reference doesn't change
|
||||
option = {
|
||||
type: 'variable',
|
||||
type: "variable",
|
||||
invalid: true,
|
||||
name: inner,
|
||||
value: null,
|
||||
@@ -110,7 +110,7 @@ function templateTags(
|
||||
};
|
||||
}
|
||||
|
||||
if (option.type === 'function') {
|
||||
if (option.type === "function") {
|
||||
const tokens = parseTemplate(rawTag);
|
||||
const rawValues = collectArgumentValues(tokens, option);
|
||||
const values = applyFormInputDefaults(option.args, rawValues);
|
||||
@@ -175,49 +175,49 @@ function makeFunctionLabel(
|
||||
): HTMLElement | string {
|
||||
if (fn.args.length === 0) return fn.name;
|
||||
|
||||
const $outer = document.createElement('span');
|
||||
$outer.className = 'fn';
|
||||
const $bOpen = document.createElement('span');
|
||||
$bOpen.className = 'fn-bracket';
|
||||
$bOpen.textContent = '(';
|
||||
const $outer = document.createElement("span");
|
||||
$outer.className = "fn";
|
||||
const $bOpen = document.createElement("span");
|
||||
$bOpen.className = "fn-bracket";
|
||||
$bOpen.textContent = "(";
|
||||
$outer.appendChild(document.createTextNode(fn.name));
|
||||
$outer.appendChild($bOpen);
|
||||
|
||||
const $inner = document.createElement('span');
|
||||
$inner.className = 'fn-inner';
|
||||
$inner.title = '';
|
||||
const $inner = document.createElement("span");
|
||||
$inner.className = "fn-inner";
|
||||
$inner.title = "";
|
||||
fn.previewArgs?.forEach((name: string, i: number, all: string[]) => {
|
||||
const v = String(values[name] || '');
|
||||
const v = String(values[name] || "");
|
||||
if (!v) return;
|
||||
if (all.length > 1) {
|
||||
const $c = document.createElement('span');
|
||||
$c.className = 'fn-arg-name';
|
||||
const $c = document.createElement("span");
|
||||
$c.className = "fn-arg-name";
|
||||
$c.textContent = i > 0 ? `, ${name}=` : `${name}=`;
|
||||
$inner.appendChild($c);
|
||||
}
|
||||
|
||||
const $v = document.createElement('span');
|
||||
$v.className = 'fn-arg-value';
|
||||
$v.textContent = v.includes(' ') ? `'${v}'` : v;
|
||||
const $v = document.createElement("span");
|
||||
$v.className = "fn-arg-value";
|
||||
$v.textContent = v.includes(" ") ? `'${v}'` : v;
|
||||
$inner.appendChild($v);
|
||||
});
|
||||
fn.args.forEach((a: FormInput, i: number) => {
|
||||
if (!('name' in a)) return;
|
||||
if (!("name" in a)) return;
|
||||
const v = values[a.name];
|
||||
if (v == null) return;
|
||||
if (i > 0) $inner.title += '\n';
|
||||
if (i > 0) $inner.title += "\n";
|
||||
$inner.title += `${a.name} = ${JSON.stringify(v)}`;
|
||||
});
|
||||
|
||||
if ($inner.childNodes.length === 0) {
|
||||
$inner.appendChild(document.createTextNode('…'));
|
||||
$inner.appendChild(document.createTextNode("…"));
|
||||
}
|
||||
|
||||
$outer.appendChild($inner);
|
||||
|
||||
const $bClose = document.createElement('span');
|
||||
$bClose.className = 'fn-bracket';
|
||||
$bClose.textContent = ')';
|
||||
const $bClose = document.createElement("span");
|
||||
$bClose.className = "fn-bracket";
|
||||
$bClose.textContent = ")";
|
||||
$outer.appendChild($bClose);
|
||||
|
||||
return $outer;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* oxlint-disable no-template-curly-in-string */
|
||||
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { parser } from './twig';
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parser } from "./twig";
|
||||
|
||||
function getNodeNames(input: string): string[] {
|
||||
const tree = parser.parse(input);
|
||||
const nodes: string[] = [];
|
||||
const cursor = tree.cursor();
|
||||
do {
|
||||
if (cursor.name !== 'Template') {
|
||||
if (cursor.name !== "Template") {
|
||||
nodes.push(cursor.name);
|
||||
}
|
||||
} while (cursor.next());
|
||||
@@ -16,93 +16,93 @@ function getNodeNames(input: string): string[] {
|
||||
}
|
||||
|
||||
function hasTag(input: string): boolean {
|
||||
return getNodeNames(input).includes('Tag');
|
||||
return getNodeNames(input).includes("Tag");
|
||||
}
|
||||
|
||||
function hasError(input: string): boolean {
|
||||
return getNodeNames(input).includes('⚠');
|
||||
return getNodeNames(input).includes("⚠");
|
||||
}
|
||||
|
||||
describe('twig grammar', () => {
|
||||
describe('${[var]} format (valid template tags)', () => {
|
||||
test('parses simple variable as Tag', () => {
|
||||
expect(hasTag('${[var]}')).toBe(true);
|
||||
expect(hasError('${[var]}')).toBe(false);
|
||||
describe("twig grammar", () => {
|
||||
describe("${[var]} format (valid template tags)", () => {
|
||||
test("parses simple variable as Tag", () => {
|
||||
expect(hasTag("${[var]}")).toBe(true);
|
||||
expect(hasError("${[var]}")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses variable with whitespace as Tag', () => {
|
||||
expect(hasTag('${[ var ]}')).toBe(true);
|
||||
expect(hasError('${[ var ]}')).toBe(false);
|
||||
test("parses variable with whitespace as Tag", () => {
|
||||
expect(hasTag("${[ var ]}")).toBe(true);
|
||||
expect(hasError("${[ var ]}")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses embedded variable as Tag', () => {
|
||||
expect(hasTag('hello ${[name]} world')).toBe(true);
|
||||
expect(hasError('hello ${[name]} world')).toBe(false);
|
||||
test("parses embedded variable as Tag", () => {
|
||||
expect(hasTag("hello ${[name]} world")).toBe(true);
|
||||
expect(hasError("hello ${[name]} world")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses function call as Tag', () => {
|
||||
expect(hasTag('${[fn()]}')).toBe(true);
|
||||
expect(hasError('${[fn()]}')).toBe(false);
|
||||
test("parses function call as Tag", () => {
|
||||
expect(hasTag("${[fn()]}")).toBe(true);
|
||||
expect(hasError("${[fn()]}")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('${var} format (should be plain text, not tags)', () => {
|
||||
test('parses ${var} as plain Text without errors', () => {
|
||||
expect(hasTag('${var}')).toBe(false);
|
||||
expect(hasError('${var}')).toBe(false);
|
||||
describe("${var} format (should be plain text, not tags)", () => {
|
||||
test("parses ${var} as plain Text without errors", () => {
|
||||
expect(hasTag("${var}")).toBe(false);
|
||||
expect(hasError("${var}")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses embedded ${var} as plain Text', () => {
|
||||
expect(hasTag('hello ${name} world')).toBe(false);
|
||||
expect(hasError('hello ${name} world')).toBe(false);
|
||||
test("parses embedded ${var} as plain Text", () => {
|
||||
expect(hasTag("hello ${name} world")).toBe(false);
|
||||
expect(hasError("hello ${name} world")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses JSON with ${var} as plain Text', () => {
|
||||
test("parses JSON with ${var} as plain Text", () => {
|
||||
const json = '{"key": "${value}"}';
|
||||
expect(hasTag(json)).toBe(false);
|
||||
expect(hasError(json)).toBe(false);
|
||||
});
|
||||
|
||||
test('parses multiple ${var} as plain Text', () => {
|
||||
expect(hasTag('${a} and ${b}')).toBe(false);
|
||||
expect(hasError('${a} and ${b}')).toBe(false);
|
||||
test("parses multiple ${var} as plain Text", () => {
|
||||
expect(hasTag("${a} and ${b}")).toBe(false);
|
||||
expect(hasError("${a} and ${b}")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed content', () => {
|
||||
test('distinguishes ${var} from ${[var]} in same string', () => {
|
||||
const input = '${plain} and ${[tag]}';
|
||||
describe("mixed content", () => {
|
||||
test("distinguishes ${var} from ${[var]} in same string", () => {
|
||||
const input = "${plain} and ${[tag]}";
|
||||
expect(hasTag(input)).toBe(true);
|
||||
expect(hasError(input)).toBe(false);
|
||||
});
|
||||
|
||||
test('parses JSON with ${[var]} as having Tag', () => {
|
||||
test("parses JSON with ${[var]} as having Tag", () => {
|
||||
const json = '{"key": "${[value]}"}';
|
||||
expect(hasTag(json)).toBe(true);
|
||||
expect(hasError(json)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
test('handles $ at end of string', () => {
|
||||
expect(hasError('hello$')).toBe(false);
|
||||
expect(hasTag('hello$')).toBe(false);
|
||||
describe("edge cases", () => {
|
||||
test("handles $ at end of string", () => {
|
||||
expect(hasError("hello$")).toBe(false);
|
||||
expect(hasTag("hello$")).toBe(false);
|
||||
});
|
||||
|
||||
test('handles ${ at end of string without crash', () => {
|
||||
test("handles ${ at end of string without crash", () => {
|
||||
// Incomplete syntax may produce errors, but should not crash
|
||||
expect(() => parser.parse('hello${')).not.toThrow();
|
||||
expect(() => parser.parse("hello${")).not.toThrow();
|
||||
});
|
||||
|
||||
test('handles ${[ without closing without crash', () => {
|
||||
test("handles ${[ without closing without crash", () => {
|
||||
// Unclosed tag may produce partial match, but should not crash
|
||||
expect(() => parser.parse('${[unclosed')).not.toThrow();
|
||||
expect(() => parser.parse("${[unclosed")).not.toThrow();
|
||||
});
|
||||
|
||||
test('handles empty ${[]}', () => {
|
||||
test("handles empty ${[]}", () => {
|
||||
// Empty tags may or may not be valid depending on grammar
|
||||
// Just ensure no crash
|
||||
expect(() => parser.parse('${[]}')).not.toThrow();
|
||||
expect(() => parser.parse("${[]}")).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LocalTokenGroup, LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import { LocalTokenGroup, LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"!^QQOPOOOOOO'#C_'#C_OYOQO'#C^OOOO'#Cc'#CcQQOPOOOOOO'#Cd'#CdO_OQO,58xOOOO-E6a-E6aOOOO-E6b-E6bOOOO1G.d1G.d",
|
||||
stateData: 'g~OUROYPO~OSTO~OSTOTXO~O',
|
||||
goto: 'nXPPY^PPPbhTROSTQOSQSORVSQUQRWU',
|
||||
nodeNames: '⚠ Template Tag TagOpen TagContent TagClose Text',
|
||||
stateData: "g~OUROYPO~OSTO~OSTOTXO~O",
|
||||
goto: "nXPPY^PPPbhTROSTQOSQSORVSQUQRWU",
|
||||
nodeNames: "⚠ Template Tag TagOpen TagContent TagClose Text",
|
||||
maxTerm: 10,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 2,
|
||||
tokenData:
|
||||
"#{~RTOtbtu!zu;'Sb;'S;=`!o<%lOb~gTU~Otbtuvu;'Sb;'S;=`!o<%lOb~yVO#ob#o#p!`#p;'Sb;'S;=`!o<%l~b~Ob~~!u~!cSO!}b#O;'Sb;'S;=`!o<%lOb~!rP;=`<%lb~!zOU~~!}VO#ob#o#p#d#p;'Sb;'S;=`!o<%l~b~Ob~~!u~#gTO!}b!}#O#v#O;'Sb;'S;=`!o<%lOb~#{OY~",
|
||||
tokenizers: [1, new LocalTokenGroup('b~RP#P#QU~XP#q#r[~aOT~~', 17, 4)],
|
||||
tokenizers: [1, new LocalTokenGroup("b~RP#P#QU~XP#q#r[~aOT~~", 17, 4)],
|
||||
topRules: { Template: [0, 1] },
|
||||
tokenPrec: 0,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FormInput, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import type { Tokens } from '@yaakapp-internal/templates';
|
||||
import type { FormInput, TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
import type { Tokens } from "@yaakapp-internal/templates";
|
||||
|
||||
/**
|
||||
* Process the initial tokens from the template and merge those with the default values pulled from
|
||||
@@ -8,21 +8,21 @@ import type { Tokens } from '@yaakapp-internal/templates';
|
||||
export function collectArgumentValues(initialTokens: Tokens, templateFunction: TemplateFunction) {
|
||||
const initial: Record<string, string | boolean> = {};
|
||||
const initialArgs =
|
||||
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
||||
initialTokens.tokens[0]?.type === "tag" && initialTokens.tokens[0]?.val.type === "fn"
|
||||
? initialTokens.tokens[0]?.val.args
|
||||
: [];
|
||||
|
||||
const processArg = (arg: FormInput) => {
|
||||
if ('inputs' in arg && arg.inputs) {
|
||||
if ("inputs" in arg && arg.inputs) {
|
||||
arg.inputs.forEach(processArg);
|
||||
}
|
||||
if (!('name' in arg)) return;
|
||||
if (!("name" in arg)) return;
|
||||
|
||||
const initialArg = initialArgs.find((a) => a.name === arg.name);
|
||||
const initialArgValue =
|
||||
initialArg?.value.type === 'str'
|
||||
initialArg?.value.type === "str"
|
||||
? initialArg?.value.text
|
||||
: initialArg?.value.type === 'bool'
|
||||
: initialArg?.value.type === "bool"
|
||||
? initialArg.value.value
|
||||
: undefined;
|
||||
const value = initialArgValue ?? arg.defaultValue;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { genericCompletion } from '../genericCompletion';
|
||||
import { genericCompletion } from "../genericCompletion";
|
||||
|
||||
export const completions = genericCompletion({
|
||||
options: [
|
||||
{ label: 'http://', type: 'constant' },
|
||||
{ label: 'https://', type: 'constant' },
|
||||
{ label: "http://", type: "constant" },
|
||||
{ label: "https://", type: "constant" },
|
||||
],
|
||||
minMatch: 1,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parser } from './url';
|
||||
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
||||
import { parser } from "./url";
|
||||
|
||||
const urlLanguage = LRLanguage.define({
|
||||
name: 'url',
|
||||
name: "url",
|
||||
parser,
|
||||
languageData: {},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
Protocol: t.comment,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"!|OQOPOOQYOPOOOTOPOOObOQO'#CdOjOPO'#C`OuOSO'#CcQOOOOOQ]OPOOOOOO,59O,59OOOOO-E6b-E6bOzOPO,58}O!SOSO'#CeO!XOPO1G.iOOOO,59P,59POOOO-E6c-E6c",
|
||||
stateData: '!g~OQQORPO~OZRO[TO~OTWOUWO~OZROYSX[SX~O]YO~O^ZOYVa~O]]O~O^ZOYVi~OQRTUT~',
|
||||
goto: 'nYPPPPZPP^bhRVPTUPVQSPRXSQ[YR^[',
|
||||
nodeNames: '⚠ url Protocol Host Path Placeholder PathSegment Query',
|
||||
stateData: "!g~OQQORPO~OZRO[TO~OTWOUWO~OZROYSX[SX~O]YO~O^ZOYVa~O]]O~O^ZOYVi~OQRTUT~",
|
||||
goto: "nYPPPPZPP^bhRVPTUPVQSPRXSQ[YR^[",
|
||||
nodeNames: "⚠ url Protocol Host Path Placeholder PathSegment Query",
|
||||
maxTerm: 14,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
|
||||
Reference in New Issue
Block a user