mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-31 14:33:18 +02:00
Hacky implementation of variable autocomplete
This commit is contained in:
@@ -20,7 +20,6 @@ interface Props {
|
|||||||
|
|
||||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||||
const environments = useEnvironments();
|
const environments = useEnvironments();
|
||||||
const updateEnvironment = useUpdateEnvironment();
|
|
||||||
const activeRequest = useActiveRequest();
|
const activeRequest = useActiveRequest();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
|
||||||
@@ -82,7 +81,11 @@ const EnvironmentList = function({ environment }: EnvironmentListProps) {
|
|||||||
className='w-full h-[400px] !bg-gray-50'
|
className='w-full h-[400px] !bg-gray-50'
|
||||||
defaultValue={JSON.stringify(environment.data, null, 2)}
|
defaultValue={JSON.stringify(environment.data, null, 2)}
|
||||||
onChange={data => {
|
onChange={data => {
|
||||||
updateEnvironment.mutate({ data: JSON.parse(data) });
|
try {
|
||||||
|
updateEnvironment.mutate({ data: JSON.parse(data) });
|
||||||
|
} catch (err) {
|
||||||
|
// That's okay
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import './Editor.css';
|
|||||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||||
import type { GenericCompletionConfig } from './genericCompletion';
|
import type { GenericCompletionConfig } from './genericCompletion';
|
||||||
import { singleLineExt } from './singleLine';
|
import { singleLineExt } from './singleLine';
|
||||||
|
import { useEnvironments } from '../../../hooks/useEnvironments';
|
||||||
|
|
||||||
// Export some things so all the code-split parts are in this file
|
// Export some things so all the code-split parts are in this file
|
||||||
export { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities';
|
export { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities';
|
||||||
@@ -64,6 +65,9 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
|||||||
}: EditorProps,
|
}: EditorProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
|
const environments = useEnvironments();
|
||||||
|
const environment = environments[0] ?? null;
|
||||||
|
|
||||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||||
useImperativeHandle(ref, () => cm.current?.view);
|
useImperativeHandle(ref, () => cm.current?.view);
|
||||||
|
|
||||||
@@ -108,9 +112,9 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cm.current === null) return;
|
if (cm.current === null) return;
|
||||||
const { view, languageCompartment } = cm.current;
|
const { view, languageCompartment } = cm.current;
|
||||||
const ext = getLanguageExtension({ contentType, useTemplating, autocomplete });
|
const ext = getLanguageExtension({ contentType, environment, useTemplating, autocomplete });
|
||||||
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||||
}, [contentType, autocomplete, useTemplating]);
|
}, [contentType, autocomplete, useTemplating, environment]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cm.current === null) return;
|
if (cm.current === null) return;
|
||||||
@@ -131,7 +135,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
|||||||
let view: EditorView;
|
let view: EditorView;
|
||||||
try {
|
try {
|
||||||
const languageCompartment = new Compartment();
|
const languageCompartment = new Compartment();
|
||||||
const langExt = getLanguageExtension({ contentType, useTemplating, autocomplete });
|
const langExt = getLanguageExtension({ contentType, useTemplating, autocomplete, environment });
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: `${defaultValue ?? ''}`,
|
doc: `${defaultValue ?? ''}`,
|
||||||
@@ -238,21 +242,21 @@ function getExtensions({
|
|||||||
: []),
|
: []),
|
||||||
...(singleLine
|
...(singleLine
|
||||||
? [
|
? [
|
||||||
EditorView.domEventHandlers({
|
EditorView.domEventHandlers({
|
||||||
focus: (e, view) => {
|
focus: (_, view) => {
|
||||||
// select all text on focus, like a regular input does
|
// select all text on focus, like a regular input does
|
||||||
view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
|
view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
|
||||||
},
|
},
|
||||||
keydown: (e) => {
|
keydown: (e) => {
|
||||||
// Submit nearest form on enter if there is one
|
// Submit nearest form on enter if there is one
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const el = e.currentTarget as HTMLElement;
|
const el = e.currentTarget as HTMLElement;
|
||||||
const form = el.closest('form');
|
const form = el.closest('form');
|
||||||
form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|
||||||
// Handle onFocus
|
// Handle onFocus
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import type { EditorProps } from './index';
|
|||||||
import { text } from './text/extension';
|
import { text } from './text/extension';
|
||||||
import { twig } from './twig/extension';
|
import { twig } from './twig/extension';
|
||||||
import { url } from './url/extension';
|
import { url } from './url/extension';
|
||||||
|
import type { Environment } from '../../../lib/models';
|
||||||
|
|
||||||
export const myHighlightStyle = HighlightStyle.define([
|
export const myHighlightStyle = HighlightStyle.define([
|
||||||
{
|
{
|
||||||
@@ -95,8 +96,9 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
|
|||||||
export function getLanguageExtension({
|
export function getLanguageExtension({
|
||||||
contentType,
|
contentType,
|
||||||
useTemplating = false,
|
useTemplating = false,
|
||||||
|
environment,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
}: Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
|
}: { environment: Environment | null } & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
|
||||||
if (contentType === 'application/graphql') {
|
if (contentType === 'application/graphql') {
|
||||||
return graphql();
|
return graphql();
|
||||||
}
|
}
|
||||||
@@ -106,7 +108,7 @@ export function getLanguageExtension({
|
|||||||
return base ? base : [];
|
return base ? base : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return twig(base, autocomplete);
|
return twig(base, environment, autocomplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const baseExtensions = [
|
export const baseExtensions = [
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ export function genericCompletion({ options, minMatch = 1 }: GenericCompletionCo
|
|||||||
if (!matchedMinimumLength && !context.explicit) return null;
|
if (!matchedMinimumLength && !context.explicit) return null;
|
||||||
|
|
||||||
const optionsWithoutExactMatches = options.filter((o) => o.label !== toMatch.text);
|
const optionsWithoutExactMatches = options.filter((o) => o.label !== toMatch.text);
|
||||||
return { from: toMatch.from, options: optionsWithoutExactMatches, info: 'hello' };
|
return { from: toMatch.from, options: optionsWithoutExactMatches, info: 'hello', };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export function singleLineExt() {
|
|||||||
(tr: Transaction): TransactionSpec | TransactionSpec[] => {
|
(tr: Transaction): TransactionSpec | TransactionSpec[] => {
|
||||||
if (!tr.isUserEvent('input')) return tr;
|
if (!tr.isUserEvent('input')) return tr;
|
||||||
|
|
||||||
const trs: TransactionSpec[] = [];
|
const specs: TransactionSpec[] = [];
|
||||||
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
tr.changes.iterChanges((_, toA, fromB, toB, inserted) => {
|
||||||
let insert = '';
|
let insert = '';
|
||||||
let newlinesRemoved = 0;
|
let newlinesRemoved = 0;
|
||||||
for (const line of inserted) {
|
for (const line of inserted) {
|
||||||
@@ -21,9 +21,10 @@ export function singleLineExt() {
|
|||||||
const selection = EditorSelection.create([cursor], 0);
|
const selection = EditorSelection.create([cursor], 0);
|
||||||
|
|
||||||
const changes = [{ from: fromB, to: toA, insert }];
|
const changes = [{ from: fromB, to: toA, insert }];
|
||||||
trs.push({ ...tr, selection, changes });
|
specs.push({ ...tr, selection, changes });
|
||||||
});
|
});
|
||||||
return trs;
|
|
||||||
|
return specs;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,44 +3,50 @@ import type { CompletionContext } from '@codemirror/autocomplete';
|
|||||||
const openTag = '${[ ';
|
const openTag = '${[ ';
|
||||||
const closeTag = ' ]}';
|
const closeTag = ' ]}';
|
||||||
|
|
||||||
const variables: { name: string }[] = [
|
interface TwigCompletionOption {
|
||||||
{ name: 'foo' }
|
name: string;
|
||||||
];
|
}
|
||||||
|
|
||||||
|
export interface TwigCompletionConfig {
|
||||||
|
options: TwigCompletionOption[];
|
||||||
|
}
|
||||||
|
|
||||||
const MIN_MATCH_VAR = 2;
|
const MIN_MATCH_VAR = 2;
|
||||||
const MIN_MATCH_NAME = 3;
|
const MIN_MATCH_NAME = 3;
|
||||||
|
|
||||||
export function completions(context: CompletionContext) {
|
export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||||
const toStartOfName = context.matchBefore(/\w*/);
|
return function completions(context: CompletionContext) {
|
||||||
const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*\w*/);
|
const toStartOfName = context.matchBefore(/\w*/);
|
||||||
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
|
const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*\w*/);
|
||||||
|
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
|
||||||
|
|
||||||
if (toMatch === null) return null;
|
if (toMatch === null) return null;
|
||||||
|
|
||||||
const matchLen = toMatch.to - toMatch.from;
|
const matchLen = toMatch.to - toMatch.from;
|
||||||
|
|
||||||
const failedVarLen = toStartOfVariable !== null && matchLen < MIN_MATCH_VAR;
|
const failedVarLen = toStartOfVariable !== null && matchLen < MIN_MATCH_VAR;
|
||||||
if (failedVarLen && !context.explicit) {
|
if (failedVarLen && !context.explicit) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const failedNameLen = toStartOfVariable === null && matchLen < MIN_MATCH_NAME;
|
||||||
|
if (failedNameLen && !context.explicit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Figure out how to make autocomplete stay open if opened explicitly. It sucks when you explicitly
|
||||||
|
// open it, then it closes when you type the next character.
|
||||||
|
return {
|
||||||
|
from: toMatch.from,
|
||||||
|
options: options
|
||||||
|
.map((v) => ({
|
||||||
|
label: toStartOfVariable ? `${openTag}${v.name}${closeTag}` : v.name,
|
||||||
|
apply: `${openTag}${v.name}${closeTag}`,
|
||||||
|
type: 'variable',
|
||||||
|
matchLen,
|
||||||
|
}))
|
||||||
|
// Filter out exact matches
|
||||||
|
.filter((o) => o.label !== toMatch.text),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const failedNameLen = toStartOfVariable === null && matchLen < MIN_MATCH_NAME;
|
|
||||||
if (failedNameLen && !context.explicit) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Figure out how to make autocomplete stay open if opened explicitly. It sucks when you explicitly
|
|
||||||
// open it, then it closes when you type the next character.
|
|
||||||
return {
|
|
||||||
from: toMatch.from,
|
|
||||||
options: variables
|
|
||||||
.map((v) => ({
|
|
||||||
label: toStartOfVariable ? `${openTag}${v.name}${closeTag}` : v.name,
|
|
||||||
apply: `${openTag}${v.name}${closeTag}`,
|
|
||||||
type: 'variable',
|
|
||||||
matchLen,
|
|
||||||
}))
|
|
||||||
// Filter out exact matches
|
|
||||||
.filter((o) => o.label !== toMatch.text),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import type { GenericCompletionConfig } from '../genericCompletion';
|
|||||||
import { genericCompletion } from '../genericCompletion';
|
import { genericCompletion } from '../genericCompletion';
|
||||||
import { placeholders } from '../placeholder';
|
import { placeholders } from '../placeholder';
|
||||||
import { textLanguageName } from '../text/extension';
|
import { textLanguageName } from '../text/extension';
|
||||||
import { completions } from './completion';
|
import { twigCompletion } from './completion';
|
||||||
import { parser as twigParser } from './twig';
|
import { parser as twigParser } from './twig';
|
||||||
|
import type { Environment } from '../../../../lib/models';
|
||||||
|
|
||||||
|
export function twig(base: LanguageSupport, environment: Environment | null, autocomplete?: GenericCompletionConfig) {
|
||||||
|
// TODO: fill variables here
|
||||||
|
const data = environment?.data ?? {};
|
||||||
|
const options = Object.keys(data).map(key => ({ name: key }));
|
||||||
|
const completions = twigCompletion({ options });
|
||||||
|
|
||||||
export function twig(base: LanguageSupport, autocomplete?: GenericCompletionConfig) {
|
|
||||||
const language = mixLanguage(base);
|
const language = mixLanguage(base);
|
||||||
const completion = language.data.of({ autocomplete: completions });
|
const completion = language.data.of({ autocomplete: completions });
|
||||||
const completionBase = base.language.data.of({ autocomplete: completions });
|
const completionBase = base.language.data.of({ autocomplete: completions });
|
||||||
|
|||||||
Reference in New Issue
Block a user