mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:13:51 +01:00
Rename, fix autocomplete and singleline, etc...
This commit is contained in:
@@ -27,9 +27,10 @@
|
||||
|
||||
.cm-editor .placeholder-widget {
|
||||
background-color: hsl(var(--color-blue-400));
|
||||
text-shadow: 0 0 0.2em black;
|
||||
padding: 0.05em 0.3em;
|
||||
border-radius: 0.2em;
|
||||
color: white;
|
||||
color: hsl(var(--color-blue-900));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -55,15 +56,13 @@
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
.cm-singleline .cm-scroller {
|
||||
display: flex;
|
||||
.cm-singleline .cm-editor .cm-scroller {
|
||||
@apply flex;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.cm-editor .cm-gutters {
|
||||
background-color: hsl(var(--color-gray-50));
|
||||
border-right: 0;
|
||||
color: hsl(var(--color-gray-200));
|
||||
@apply bg-gray-50 border-r-0 text-gray-200;
|
||||
}
|
||||
|
||||
.cm-editor .cm-gutterElement {
|
||||
@@ -71,83 +70,92 @@
|
||||
}
|
||||
|
||||
.cm-editor .fold-gutter-icon {
|
||||
height: 1.5em;
|
||||
padding-top: 0.2em;
|
||||
padding-left: 0.4em;
|
||||
padding-right: 0.4em;
|
||||
cursor: pointer;
|
||||
border-radius: 0.2em;
|
||||
@apply pt-[0.3em] pl-[0.4em] px-[0.4em] h-4 cursor-pointer rounded;
|
||||
}
|
||||
|
||||
.cm-editor .fold-gutter-icon::after {
|
||||
display: block;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
border: 1px solid transparent;
|
||||
border-left-color: currentColor;
|
||||
border-bottom-color: currentColor;
|
||||
transform: rotate(-45deg);
|
||||
content: "";
|
||||
@apply block w-1.5 h-1.5 border-transparent -rotate-45
|
||||
border-l border-b border-l-[currentColor] border-b-[currentColor] content-[''];
|
||||
}
|
||||
|
||||
.cm-editor .fold-gutter-icon[data-open] {
|
||||
padding-top: 0.4em;
|
||||
padding-left: 0.2em;
|
||||
@apply pt-[0.4em] pl-[0.3em];
|
||||
}
|
||||
|
||||
.cm-editor .fold-gutter-icon[data-open]::after {
|
||||
transform: rotate(-135deg);
|
||||
@apply rotate-[-135deg];
|
||||
}
|
||||
|
||||
.cm-editor .fold-gutter-icon:hover {
|
||||
background-color: hsl(var(--color-gray-100)/0.2);
|
||||
color: hsl(var(--color-gray-400));
|
||||
@apply text-gray-400 bg-gray-100/20;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused .cm-gutters {
|
||||
color: hsl(var(--color-gray-300));
|
||||
@apply text-gray-300;
|
||||
}
|
||||
|
||||
.cm-editor .cm-foldPlaceholder {
|
||||
background-color: hsl(var(--color-gray-100));
|
||||
border: 1px solid hsl(var(--color-gray-200));
|
||||
padding: 0 0.3em;
|
||||
@apply px-2 border border-gray-200 bg-gray-100;
|
||||
}
|
||||
|
||||
.cm-editor .cm-activeLineGutter,
|
||||
.cm-editor .cm-activeLine {
|
||||
background-color: transparent;
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused .cm-activeLineGutter {
|
||||
color: hsl(var(--color-gray-800));
|
||||
@apply text-gray-800;
|
||||
}
|
||||
|
||||
.cm-editor * {
|
||||
cursor: text;
|
||||
@apply cursor-text;
|
||||
}
|
||||
|
||||
.cm-editor .cm-cursor {
|
||||
border-left: 2px solid hsl(var(--color-gray-900));
|
||||
@apply border-l-2 border-gray-800;
|
||||
}
|
||||
|
||||
.cm-editor .cm-selectionBackground {
|
||||
background-color: hsl(var(--color-gray-200));
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused .cm-selectionBackground {
|
||||
background-color: hsl(var(--color-gray-200));
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
|
||||
/* --> Add padding to container. For some reason, using padding on both adds an extra
|
||||
* 1px offset so we need to use a combination of padding and margin.
|
||||
*/
|
||||
.cm-editor .cm-gutters {
|
||||
padding-top: 0.2em;
|
||||
@apply pt-1;
|
||||
}
|
||||
|
||||
.cm-editor .cm-content {
|
||||
margin-top: 0.2em;
|
||||
@apply mt-1;
|
||||
}
|
||||
|
||||
/* <-- */
|
||||
|
||||
.cm-editor .cm-tooltip {
|
||||
@apply shadow-lg border-0 bg-background rounded overflow-hidden text-gray-900;
|
||||
}
|
||||
|
||||
.cm-editor .cm-tooltip * {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.cm-editor .cm-tooltip.cm-tooltip-autocomplete > ul {
|
||||
@apply p-1 max-h-[40vh];
|
||||
}
|
||||
|
||||
.cm-editor .cm-tooltip.cm-tooltip-autocomplete > ul > li {
|
||||
@apply cursor-default py-1 px-2 rounded-sm text-gray-500;
|
||||
}
|
||||
|
||||
.cm-editor .cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected] {
|
||||
@apply bg-gray-50 text-gray-800;
|
||||
}
|
||||
|
||||
.cm-editor .cm-tooltip.cm-tooltip-autocomplete .cm-completionIcon {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Transaction, TransactionSpec } from '@codemirror/state';
|
||||
import { Compartment, EditorSelection, EditorState, Prec } from '@codemirror/state';
|
||||
import { placeholder as placeholderExt } from '@codemirror/view';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import { keymap, placeholder as placeholderExt } from '@codemirror/view';
|
||||
import classnames from 'classnames';
|
||||
import { EditorView } from 'codemirror';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import './Editor.css';
|
||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||
import { singleLineExt } from './singleLine';
|
||||
|
||||
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||
contentType: string;
|
||||
@@ -106,43 +107,25 @@ function getExtensions({
|
||||
>) {
|
||||
const ext = getLanguageExtension({ contentType, useTemplating });
|
||||
return [
|
||||
...(singleLine
|
||||
? [
|
||||
Prec.high(
|
||||
EditorView.domEventHandlers({
|
||||
keydown: (e) => {
|
||||
// TODO: Figure out how to not have this not trigger on autocomplete selection
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onSubmit?.();
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
EditorState.transactionFilter.of(
|
||||
(tr: Transaction): TransactionSpec | TransactionSpec[] => {
|
||||
if (!tr.isUserEvent('input.paste')) return tr;
|
||||
|
||||
const trs: TransactionSpec[] = [];
|
||||
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
||||
let insert = '';
|
||||
for (const line of inserted) {
|
||||
insert += line.replace('\n', '');
|
||||
}
|
||||
const changes = [{ from: fromB, to: toA, insert }];
|
||||
// Update selection now that the text has been changed
|
||||
const selection = EditorSelection.create([EditorSelection.cursor(toB - 1)], 0);
|
||||
trs.push({ ...tr, selection, changes });
|
||||
});
|
||||
return trs;
|
||||
},
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...baseExtensions,
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||
...(singleLine ? [singleLineExt()] : []),
|
||||
...(!singleLine ? [multiLineExtensions] : []),
|
||||
...(ext ? [ext] : []),
|
||||
...(placeholder ? [placeholderExt(placeholder)] : []),
|
||||
|
||||
// Handle onSubmit
|
||||
...(onSubmit
|
||||
? [
|
||||
EditorView.domEventHandlers({
|
||||
keydown: (e) => {
|
||||
console.log('KEYDOWN', e);
|
||||
if (e.key === 'Enter') onSubmit?.();
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
// Handle onChange
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (typeof onChange === 'function' && update.docChanged) {
|
||||
onChange(update.state.doc.toString());
|
||||
|
||||
@@ -8,25 +8,30 @@ const variables = [
|
||||
{ name: 'BASE_URL' },
|
||||
{ name: 'TOKEN' },
|
||||
{ name: 'PROJECT_ID' },
|
||||
{ name: 'DUMMY' },
|
||||
{ name: 'DUMMY_2' },
|
||||
];
|
||||
|
||||
export function myCompletions(context: CompletionContext) {
|
||||
const toStartOfName = context.matchBefore(/\w*/);
|
||||
const toStartOfVariable = context.matchBefore(/\$\{.*/);
|
||||
const toStartOfName = context.explicit ? context.matchBefore(/\w*/) : context.matchBefore(/\w+/);
|
||||
const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*\w*/);
|
||||
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
|
||||
|
||||
if (toMatch === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (toMatch.from === toMatch.to && !context.explicit) {
|
||||
// Match a minimum of two characters when typing a variable ${[...]} to prevent it
|
||||
// from opening on "$"
|
||||
if (toStartOfVariable !== null && toMatch.to - toMatch.from < 2 && !context.explicit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
from: toMatch.from,
|
||||
options: variables.map((v) => ({
|
||||
label: `${openTag}${v.name}${closeTag}`,
|
||||
label: toStartOfVariable ? `${openTag}${v.name}${closeTag}` : v.name,
|
||||
apply: `${openTag}${v.name}${closeTag}`,
|
||||
type: 'variable',
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -102,13 +102,12 @@ export function getLanguageExtension({
|
||||
}
|
||||
|
||||
export const baseExtensions = [
|
||||
keymap.of([...defaultKeymap]),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
autocompletion(),
|
||||
autocompletion({ activateOnTyping: false, closeOnBlur: true }),
|
||||
syntaxHighlighting(myHighlightStyle),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
];
|
||||
|
||||
29
src-web/components/Editor/singleLine.ts
Normal file
29
src-web/components/Editor/singleLine.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Transaction, TransactionSpec } from '@codemirror/state';
|
||||
import { EditorSelection, EditorState } from '@codemirror/state';
|
||||
|
||||
export function singleLineExt() {
|
||||
return EditorState.transactionFilter.of(
|
||||
(tr: Transaction): TransactionSpec | TransactionSpec[] => {
|
||||
if (!tr.isUserEvent('input')) return tr;
|
||||
|
||||
const trs: TransactionSpec[] = [];
|
||||
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
||||
let insert = '';
|
||||
let newlinesRemoved = 0;
|
||||
for (const line of inserted) {
|
||||
const newLine = line.replace('\n', '');
|
||||
newlinesRemoved += line.length - newLine.length;
|
||||
insert += newLine;
|
||||
}
|
||||
|
||||
// Update cursor position based on how many newlines were removed
|
||||
const cursor = EditorSelection.cursor(toB - newlinesRemoved);
|
||||
const selection = EditorSelection.create([cursor], 0);
|
||||
|
||||
const changes = [{ from: fromB, to: toA, insert }];
|
||||
trs.push({ ...tr, selection, changes });
|
||||
});
|
||||
return trs;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { LRLanguage, LanguageSupport } from '@codemirror/language';
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parseMixed } from '@lezer/common';
|
||||
import { myCompletions } from '../completion/completion';
|
||||
import { placeholders } from '../widgets';
|
||||
import { parser as twigParser } from './twig';
|
||||
|
||||
export function twig(base?: LanguageSupport) {
|
||||
const parser = mixedOrPlainParser(base);
|
||||
const twigLanguage = LRLanguage.define({ name: 'twig', parser, languageData: {} });
|
||||
const completion = twigLanguage.data.of({
|
||||
const language = mixedOrPlainLanguage(base);
|
||||
const completion = language.data.of({
|
||||
autocomplete: myCompletions,
|
||||
});
|
||||
const languageSupport = new LanguageSupport(twigLanguage, [completion]);
|
||||
const languageSupport = new LanguageSupport(language, [completion]);
|
||||
|
||||
if (base) {
|
||||
const completion2 = base.language.data.of({ autocomplete: myCompletions });
|
||||
@@ -21,31 +20,22 @@ export function twig(base?: LanguageSupport) {
|
||||
}
|
||||
}
|
||||
|
||||
function mixedOrPlainParser(base?: LanguageSupport) {
|
||||
if (base === undefined) {
|
||||
return twigParser;
|
||||
function mixedOrPlainLanguage(base?: LanguageSupport): LRLanguage {
|
||||
const name = 'twig';
|
||||
|
||||
if (base == null) {
|
||||
return LRLanguage.define({ name, parser: twigParser });
|
||||
}
|
||||
|
||||
const mixedParser = twigParser.configure({
|
||||
props: [
|
||||
// Add basic folding/indent metadata
|
||||
// foldNodeProp.add({ Conditional: foldInside }),
|
||||
// indentNodeProp.add({
|
||||
// Conditional: (cx) => {
|
||||
// const closed = /^\s*\{% endif/.test(cx.textAfter);
|
||||
// return cx.lineIndent(cx.node.from) + (closed ? 0 : cx.unit);
|
||||
// },
|
||||
// }),
|
||||
],
|
||||
const parser = twigParser.configure({
|
||||
wrap: parseMixed((node) => {
|
||||
return node.type.isTop
|
||||
? {
|
||||
parser: base.language.parser,
|
||||
overlay: (node) => node.type.name === 'Text',
|
||||
}
|
||||
: null;
|
||||
if (!node.type.isTop) return null;
|
||||
return {
|
||||
parser: base.language.parser,
|
||||
overlay: (node) => node.type.name === 'Text',
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
return mixedParser;
|
||||
return LRLanguage.define({ name, parser });
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ directive {
|
||||
}
|
||||
|
||||
@tokens {
|
||||
Text { ![${[] Text? }
|
||||
Text { ![$] Text? }
|
||||
space { @whitespace+ }
|
||||
DirectiveContent { ![\]}$] DirectiveContent? }
|
||||
DirectiveContent { ![\]}] DirectiveContent? }
|
||||
@precedence { space DirectiveContent }
|
||||
"${[" "]}"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const parser = LRParser.deserialize({
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: ")a~RqOX#YX^%i^p#Ypq%iqt#Ytu'vu!}#Y!}#O$V#O#P#Y#P#Q(X#Q#o#Y#o#p$V#p#q#Y#q#r$t#r#y#Y#y#z%i#z$f#Y$f$g%i$g#BY#Y#BY#BZ%i#BZ$IS#Y$IS$I_%i$I_$I|#Y$I|$JO%i$JO$JT#Y$JT$JU%i$JU$KV#Y$KV$KW%i$KW&FU#Y&FU&FV%i&FV;'S#Y;'S;=`%c<%lO#YR#a[UPSQOt#Yu!}#Y!}#O$V#O#P#Y#P#Q$t#Q#o#Y#o#p$V#p#q#Y#q#r$t#r;'S#Y;'S;=`%c<%lO#YQ$[USQOt$Vu#P$V#Q#q$V#r;'S$V;'S;=`$n<%lO$VQ$qP;=`<%l$VP$yUUPOt$tu!}$t#O#o$t#p;'S$t;'S;=`%]<%lO$tP%`P;=`<%l$tR%fP;=`<%l#YR%rpUPYQSQOX#YX^%i^p#Ypq%iqt#Yu!}#Y!}#O$V#O#P#Y#P#Q$t#Q#o#Y#o#p$V#p#q#Y#q#r$t#r#y#Y#y#z%i#z$f#Y$f$g%i$g#BY#Y#BY#BZ%i#BZ$IS#Y$IS$I_%i$I_$I|#Y$I|$JO%i$JO$JT#Y$JT$JU%i$JU$KV#Y$KV$KW%i$KW&FU#Y&FU&FV%i&FV;'S#Y;'S;=`%c<%lO#Y~'yP#o#p'|~(PP!}#O(S~(XOR~R(^WUPOt$tu!}$t#O#o$t#p#q$t#q#r(v#r;'S$t;'S;=`%]<%lO$tR(}UTQUPOt$tu!}$t#O#o$t#p;'S$t;'S;=`%]<%lO$t",
|
||||
tokenData: ")gRRmOX!|X^$y^p!|pq$yqt!|tu&}u#P!|#P#Q(k#Q#q!|#q#r$[#r#y!|#y#z$y#z$f!|$f$g$y$g#BY!|#BY#BZ$y#BZ$IS!|$IS$I_$y$I_$I|!|$I|$JO$y$JO$JT!|$JT$JU$y$JU$KV!|$KV$KW$y$KW&FU!|&FU&FV$y&FV;'S!|;'S;=`$s<%lO!|R#TXUPSQOt!|tu#pu#P!|#P#Q$[#Q#q!|#q#r$[#r;'S!|;'S;=`$s<%lO!|Q#uTSQO#P#p#Q#q#p#r;'S#p;'S;=`$U<%lO#pQ$XP;=`<%l#pP$aSUPOt$[u;'S$[;'S;=`$m<%lO$[P$pP;=`<%l$[R$vP;=`<%l!|R%SmUPYQSQOX!|X^$y^p!|pq$yqt!|tu#pu#P!|#P#Q$[#Q#q!|#q#r$[#r#y!|#y#z$y#z$f!|$f$g$y$g#BY!|#BY#BZ$y#BZ$IS!|$IS$I_$y$I_$I|!|$I|$JO$y$JO$JT!|$JT$JU$y$JU$KV!|$KV$KW$y$KW&FU!|&FU&FV$y&FV;'S!|;'S;=`$s<%lO!|R'SVSQO#P#p#Q#o#p#o#p'i#p#q#p#r;'S#p;'S;=`$U<%lO#pR'nVSQO!}#p!}#O(T#O#P#p#Q#q#p#r;'S#p;'S;=`$U<%lO#pR([TRPSQO#P#p#Q#q#p#r;'S#p;'S;=`$U<%lO#pR(pUUPOt$[u#q$[#q#r)S#r;'S$[;'S;=`$m<%lO$[R)ZSTQUPOt$[u;'S$[;'S;=`$m<%lO$[",
|
||||
tokenizers: [0, 1],
|
||||
topRules: {"Template":[0,1]},
|
||||
tokenPrec: 25
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ResponsePane({ requestId, error }: Props) {
|
||||
},
|
||||
'-----',
|
||||
...responses.data.slice(0, 10).map((r) => ({
|
||||
label: r.status + ' - ' + r.elapsed,
|
||||
label: r.status + ' - ' + r.elapsed + ' ms',
|
||||
leftSlot: response?.id === r.id ? <Icon icon="check" /> : <></>,
|
||||
onSelect: () => setActiveResponseId(r.id),
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user