Rename, fix autocomplete and singleline, etc...

This commit is contained in:
Gregory Schier
2023-03-02 10:42:43 -08:00
parent 70f534f1d8
commit 0ccceaac77
27 changed files with 128 additions and 114 deletions

View File

@@ -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;
}

View File

@@ -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());

View File

@@ -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',
})),
};

View File

@@ -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),
];

View 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;
},
);
}

View File

@@ -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 });
}

View File

@@ -9,9 +9,9 @@ directive {
}
@tokens {
Text { ![${[] Text? }
Text { ![$] Text? }
space { @whitespace+ }
DirectiveContent { ![\]}$] DirectiveContent? }
DirectiveContent { ![\]}] DirectiveContent? }
@precedence { space DirectiveContent }
"${[" "]}"
}

View File

@@ -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

View File

@@ -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),
})),