From 29d1f687d119fd08891a81db84dbd3638668b7bc Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 28 Feb 2023 22:54:54 -0800 Subject: [PATCH] Autocomplete, and more CM stuff! --- .eslintrc.cjs | 5 ++ src-tauri/src/main.rs | 44 ++++++++-- src-web/App.tsx | 12 ++- src-web/components/ButtonLink.tsx | 3 +- src-web/components/Editor/Editor.tsx | 37 +++++---- .../Editor/completion/completion.ts | 34 ++++++++ src-web/components/Editor/extensions.ts | 83 ++++++++++--------- .../twig/{twig-highlight.ts => highlight.ts} | 4 +- src-web/components/Editor/twig/twig.grammar | 12 ++- src-web/components/Editor/twig/twig.ts | 8 +- src-web/components/Editor/url/extension.ts | 39 ++------- src-web/components/Editor/url/highlight.ts | 9 ++ src-web/components/Editor/url/url.grammar | 31 +++---- src-web/components/Editor/url/url.terms.ts | 10 +-- src-web/components/Editor/url/url.ts | 20 +++-- src-web/components/Editor/widgets.ts | 13 +-- src-web/components/Input.tsx | 5 +- src-web/components/ResponsePane.tsx | 21 +++-- src-web/components/Stacks.tsx | 5 +- src-web/components/UrlBar.tsx | 3 +- 20 files changed, 220 insertions(+), 178 deletions(-) create mode 100644 src-web/components/Editor/completion/completion.ts rename src-web/components/Editor/twig/{twig-highlight.ts => highlight.ts} (66%) create mode 100644 src-web/components/Editor/url/highlight.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a878337b..e6d78a23 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,5 +21,10 @@ module.exports = { }, rules: { "react/react-in-jsx-scope": "off", + "@typescript-eslint/consistent-type-imports": ["error", { + prefer: "type-imports", + disallowTypeAnnotations: true, + fixStyle: "separate-type-imports", + }] }, }; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 040f94f0..188af3ee 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -17,6 +17,7 @@ use reqwest::redirect::Policy; use sqlx::migrate::Migrator; use sqlx::sqlite::SqlitePoolOptions; use sqlx::{Pool, Sqlite}; +use tauri::regex::Regex; use tauri::{AppHandle, State, Wry}; use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent}; use tokio::sync::Mutex; @@ -61,15 +62,33 @@ async fn send_request( .expect("Failed to get request"); let start = std::time::Instant::now(); - let mut abs_url = req.url.to_string(); - if !abs_url.starts_with("http://") && !abs_url.starts_with("https://") { - abs_url = format!("http://{}", req.url); + let mut url_string = req.url.to_string(); + + let mut variables = HashMap::new(); + variables.insert("PROJECT_ID", "project_123"); + variables.insert("TOKEN", "s3cret"); + variables.insert("DOMAIN", "schier.co"); + variables.insert("BASE_URL", "https://schier.co"); + + let re = Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}").expect("Failed to create regex"); + url_string = re + .replace(&url_string, |caps: &tauri::regex::Captures| { + let key = caps.get(1).unwrap().as_str(); + match variables.get(key) { + Some(v) => v, + None => "", + } + }) + .to_string(); + + if !url_string.starts_with("http://") && !url_string.starts_with("https://") { + url_string = format!("http://{}", url_string); } let client = reqwest::Client::builder() .redirect(Policy::none()) .build() - .unwrap(); + .expect("Failed to build client"); let mut headers = HeaderMap::new(); headers.insert(USER_AGENT, HeaderValue::from_static("reqwest")); @@ -79,14 +98,21 @@ async fn send_request( HeaderValue::from_str(models::generate_id("x").as_str()).expect("Failed to create header"), ); - let m = Method::from_bytes(req.method.to_uppercase().as_bytes()).unwrap(); - let builder = client.request(m, abs_url.to_string()).headers(headers); + let m = + Method::from_bytes(req.method.to_uppercase().as_bytes()).expect("Failed to create method"); + let builder = client.request(m, url_string.to_string()).headers(headers); - let sendable_req = match req.body { + let sendable_req_result = match req.body { Some(b) => builder.body(b).build(), None => builder.build(), - } - .expect("Failed to build request"); + }; + + let sendable_req = match sendable_req_result { + Ok(r) => r, + Err(e) => { + return Err(e.to_string()); + } + }; let resp = client.execute(sendable_req).await; diff --git a/src-web/App.tsx b/src-web/App.tsx index d4c40c9e..53468636 100644 --- a/src-web/App.tsx +++ b/src-web/App.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import Editor from './components/Editor/Editor'; import { HStack, VStack } from './components/Stacks'; import { WindowDragRegion } from './components/WindowDragRegion'; @@ -40,11 +40,17 @@ function App() { return () => document.documentElement.removeEventListener('keypress', listener); }, []); + const [screenWidth, setScreenWidth] = useState(window.innerWidth); + useEffect(() => { + console.log('SCREEN WIDTH', document.documentElement.clientWidth); + window.addEventListener('resize', () => setScreenWidth(window.innerWidth)); + }, []); + return (
{request && ( - + 700 ? 2 : 1} rows={screenWidth > 700 ? 1 : 2}> Test Request @@ -61,7 +67,7 @@ function App() { sendRequest={sendRequest.mutate} /> & LinkProps; diff --git a/src-web/components/Editor/Editor.tsx b/src-web/components/Editor/Editor.tsx index cc02c41a..83412883 100644 --- a/src-web/components/Editor/Editor.tsx +++ b/src-web/components/Editor/Editor.tsx @@ -1,11 +1,12 @@ -import './Editor.css'; -import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react'; -import { EditorView } from 'codemirror'; -import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions'; -import type { TransactionSpec } from '@codemirror/state'; -import { Compartment, EditorSelection, EditorState, Transaction } from '@codemirror/state'; -import classnames from 'classnames'; import { autocompletion } from '@codemirror/autocomplete'; +import type { Transaction, TransactionSpec } from '@codemirror/state'; +import { Compartment, EditorSelection, EditorState, Prec } from '@codemirror/state'; +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'; interface Props extends Omit, 'onChange'> { contentType: string; @@ -96,18 +97,19 @@ function getExtensions({ }: Pick) { const ext = getLanguageExtension({ contentType, useTemplating }); return [ - autocompletion(), ...(singleLine ? [ - EditorView.domEventHandlers({ - keydown: (e) => { - // TODO: Figure out how to not have this mess up autocomplete - if (e.key === 'Enter') { - e.preventDefault(); - onSubmit?.(); - } - }, - }), + 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')) { @@ -117,7 +119,6 @@ function getExtensions({ // let addedNewline = false; const trs: TransactionSpec[] = []; tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => { - // console.log('CHANGE', { fromA, toA }, { fromB, toB }, inserted); let insert = ''; for (const line of inserted) { insert += line.replace('\n', ''); diff --git a/src-web/components/Editor/completion/completion.ts b/src-web/components/Editor/completion/completion.ts new file mode 100644 index 00000000..3a0ff062 --- /dev/null +++ b/src-web/components/Editor/completion/completion.ts @@ -0,0 +1,34 @@ +import type { CompletionContext } from '@codemirror/autocomplete'; + +const openTag = '${[ '; +const closeTag = ' ]}'; + +const variables = [ + { name: 'DOMAIN' }, + { name: 'BASE_URL' }, + { name: 'TOKEN' }, + { name: 'PROJECT_ID' }, +]; + +export function myCompletions(context: CompletionContext) { + // console.log('COMPLETE', context); + const toStartOfName = context.matchBefore(/\w*/); + const toStartOfVariable = context.matchBefore(/\$\{.*/); + const toMatch = toStartOfVariable ?? toStartOfName ?? null; + + if (toMatch === null) { + return null; + } + + if (toMatch.from === toMatch.to && !context.explicit) { + return null; + } + + return { + from: toMatch.from, + options: variables.map((v) => ({ + label: `${openTag}${v.name}${closeTag}`, + type: 'variable', + })), + }; +} diff --git a/src-web/components/Editor/extensions.ts b/src-web/components/Editor/extensions.ts index 21f76a02..4eaeb7e8 100644 --- a/src-web/components/Editor/extensions.ts +++ b/src-web/components/Editor/extensions.ts @@ -1,18 +1,27 @@ -import { parser as twigParser } from './twig/twig'; +import { + autocompletion, + closeBrackets, + closeBracketsKeymap, + completionKeymap, +} from '@codemirror/autocomplete'; +import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; +import { html } from '@codemirror/lang-html'; +import { javascript } from '@codemirror/lang-javascript'; +import { json } from '@codemirror/lang-json'; +import { xml } from '@codemirror/lang-xml'; import { bracketMatching, foldGutter, - foldInside, foldKeymap, - foldNodeProp, HighlightStyle, - indentNodeProp, indentOnInput, LanguageSupport, LRLanguage, syntaxHighlighting, } from '@codemirror/language'; import { lintKeymap } from '@codemirror/lint'; +import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'; +import { EditorState } from '@codemirror/state'; import { crosshairCursor, drawSelection, @@ -24,23 +33,12 @@ import { lineNumbers, rectangularSelection, } from '@codemirror/view'; -import { html } from '@codemirror/lang-html'; -import { xml } from '@codemirror/lang-xml'; import { parseMixed } from '@lezer/common'; -import { EditorState } from '@codemirror/state'; -import { json } from '@codemirror/lang-json'; -import { javascript } from '@codemirror/lang-javascript'; import { tags as t } from '@lezer/highlight'; -import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; -import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'; -import { - autocompletion, - closeBrackets, - closeBracketsKeymap, - completionKeymap, -} from '@codemirror/autocomplete'; -import { placeholders } from './widgets'; +import { myCompletions } from './completion/completion'; +import { parser as twigParser } from './twig/twig'; import { url } from './url/extension'; +import { placeholders } from './widgets'; export const myHighlightStyle = HighlightStyle.define([ { @@ -82,13 +80,13 @@ export const myHighlightStyle = HighlightStyle.define([ // { tag: t.invalid, color: '#f00' }, // ]); -const syntaxExtensions: Record = { - 'application/json': { base: json(), ext: [] }, - 'application/javascript': { base: javascript(), ext: [] }, - 'text/html': { base: html(), ext: [] }, - 'application/xml': { base: xml(), ext: [] }, - 'text/xml': { base: xml(), ext: [] }, - url: { base: url(), ext: [] }, +const syntaxExtensions: Record = { + 'application/json': json(), + 'application/javascript': javascript(), + 'text/html': html(), + 'application/xml': xml(), + 'text/xml': xml(), + url: url(), }; export function getLanguageExtension({ @@ -99,7 +97,7 @@ export function getLanguageExtension({ useTemplating?: boolean; }) { const justContentType = contentType.split(';')[0] ?? contentType; - const { base, ext } = syntaxExtensions[justContentType] ?? { base: json(), ext: [] }; + const base = syntaxExtensions[justContentType] ?? json(); if (!useTemplating) { return [base]; } @@ -107,35 +105,44 @@ export function getLanguageExtension({ const mixedTwigParser = 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); - }, - }), + // 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); + // }, + // }), ], wrap: parseMixed((node) => { return node.type.isTop ? { parser: base.language.parser, - overlay: (node) => node.type.name == 'Text', + overlay: (node) => node.type.name === 'Text', } : null; }), }); - const twigLanguage = LRLanguage.define({ parser: mixedTwigParser }); - return [twigLanguage, placeholders, base.support, ...ext]; + const twigLanguage = LRLanguage.define({ parser: mixedTwigParser, languageData: {} }); + const completion = twigLanguage.data.of({ + autocomplete: myCompletions, + }); + const languageSupport = new LanguageSupport(twigLanguage, [completion]); + const completion2 = base.language.data.of({ autocomplete: myCompletions }); + const languageSupport2 = new LanguageSupport(base.language, [completion2]); + return [languageSupport, languageSupport2, placeholders, base.support]; } export const baseExtensions = [ + keymap.of([...defaultKeymap]), highlightSpecialChars(), history(), drawSelection(), dropCursor(), - EditorState.allowMultipleSelections.of(true), + bracketMatching(), + autocompletion(), syntaxHighlighting(myHighlightStyle), + EditorState.allowMultipleSelections.of(true), ]; export const multiLineExtensions = [ @@ -152,12 +159,10 @@ export const multiLineExtensions = [ return el; }, }), - drawSelection(), EditorState.allowMultipleSelections.of(true), indentOnInput(), bracketMatching(), closeBrackets(), - autocompletion(), rectangularSelection(), crosshairCursor(), highlightActiveLine(), diff --git a/src-web/components/Editor/twig/twig-highlight.ts b/src-web/components/Editor/twig/highlight.ts similarity index 66% rename from src-web/components/Editor/twig/twig-highlight.ts rename to src-web/components/Editor/twig/highlight.ts index 365d706c..b53a528f 100644 --- a/src-web/components/Editor/twig/twig-highlight.ts +++ b/src-web/components/Editor/twig/highlight.ts @@ -1,7 +1,7 @@ import { styleTags, tags as t } from '@lezer/highlight'; -export const twigHighlight = styleTags({ +export const highlight = styleTags({ 'if endif': t.controlKeyword, - '{{ }} {% %}': t.meta, + '${[ ]}': t.meta, DirectiveContent: t.variableName, }); diff --git a/src-web/components/Editor/twig/twig.grammar b/src-web/components/Editor/twig/twig.grammar index a0c2ca6f..60b1591d 100644 --- a/src-web/components/Editor/twig/twig.grammar +++ b/src-web/components/Editor/twig/twig.grammar @@ -1,5 +1,3 @@ -// Very crude grammar for a subset of Twig templating syntax - @top Template { (directive | Text)* } directive { @@ -7,15 +5,15 @@ directive { } @skip {space} { - Insert { "{{" DirectiveContent "}}" } + Insert { "${[" DirectiveContent "]}" } } @tokens { - Text { ![{] Text? | "{" (@eof | ![%{] Text?) } + Text { ![${[] Text? } space { @whitespace+ } - DirectiveContent { ![%}] DirectiveContent? | $[%}] (@eof | ![}] DirectiveContent?) } + DirectiveContent { ![\]}$] DirectiveContent? } @precedence { space DirectiveContent } - "{{" "}}" // "{%" "%}" + "${[" "]}" } -@external propSource twigHighlight from "./twig-highlight" +@external propSource highlight from "./highlight" diff --git a/src-web/components/Editor/twig/twig.ts b/src-web/components/Editor/twig/twig.ts index 5a173501..3057bdaa 100644 --- a/src-web/components/Editor/twig/twig.ts +++ b/src-web/components/Editor/twig/twig.ts @@ -1,17 +1,17 @@ // This file was generated by lezer-generator. You probably shouldn't edit it. import {LRParser} from "@lezer/lr" -import {twigHighlight} from "./twig-highlight" +import {highlight} from "./highlight" export const parser = LRParser.deserialize({ version: 14, states: "zQVOPOOO_QQO'#C^OOOO'#Cc'#CcQVOPOOOdQQO,58xOOOO-E6a-E6aOOOO1G.d1G.d", stateData: "l~OYOS~ORPOUQO~OSSO~OTUO~OYS~", goto: "cWPPXPPPP]TQORQRORTR", - nodeNames: "⚠ Template Insert {{ DirectiveContent }} Text", + nodeNames: "⚠ Template Insert ${[ DirectiveContent ]} Text", maxTerm: 10, - propSources: [twigHighlight], + propSources: [highlight], skippedNodes: [0], repeatNodeCount: 1, - tokenData: ",gRRmOX!|X^'u^p!|pq'uqu!|uv#pv#o!|#o#p)y#p#q!|#q#r+_#r#y!|#y#z'u#z$f!|$f$g'u$g#BY!|#BY#BZ'u#BZ$IS!|$IS$I_'u$I_$I|!|$I|$JO'u$JO$JT!|$JT$JU'u$JU$KV!|$KV$KW'u$KW&FU!|&FU&FV'u&FV;'S!|;'S;=`&f<%lO!|R#TXUPSQOu!|uv#pv#o!|#o#p$b#p#q!|#q#r#p#r;'S!|;'S;=`&f<%lO!|R#uXUPO#o!|#o#p$b#p#q!|#q#r&q#r;'S!|;'S;=`&f<%l~!|~O!|~~&aR$gZSQOu!|uv%Yv#o!|#o#p%o#p#q!|#q#r#p#r;'S!|;'S;=`&f<%l~!|~O!|~~&lQ%]UO#q%o#r;'S%o;'S;=`&Z<%l~%o~O%o~~&aQ%tVSQOu%ouv%Yv#q%o#q#r%Y#r;'S%o;'S;=`&Z<%lO%oQ&^P;=`<%l%oQ&fOSQR&iP;=`<%l!|P&qOUPP&vTUPO#o&q#o#p'V#p;'S&q;'S;=`'o<%lO&qP'YVOu&qv#o&q#p;'S&q;'S;=`'o<%l~&q~O&q~~&lP'rP;=`<%l&qR(OmUPYQSQOX!|X^'u^p!|pq'uqu!|uv#pv#o!|#o#p$b#p#q!|#q#r#p#r#y!|#y#z'u#z$f!|$f$g'u$g#BY!|#BY#BZ'u#BZ$IS!|$IS$I_'u$I_$I|!|$I|$JO'u$JO$JT!|$JT$JU'u$JU$KV!|$KV$KW'u$KW&FU!|&FU&FV'u&FV;'S!|;'S;=`&f<%lO!|R*OZSQOu!|uv%Yv#o!|#o#p*q#p#q!|#q#r#p#r;'S!|;'S;=`&f<%l~!|~O!|~~&lR*xVRPSQOu%ouv%Yv#q%o#q#r%Y#r;'S%o;'S;=`&Z<%lO%oR+dXUPO#o!|#o#p$b#p#q!|#q#r,P#r;'S!|;'S;=`&f<%l~!|~O!|~~&aR,WTTQUPO#o&q#o#p'V#p;'S&q;'S;=`'o<%lO&q", + 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", tokenizers: [0, 1], topRules: {"Template":[0,1]}, tokenPrec: 25 diff --git a/src-web/components/Editor/url/extension.ts b/src-web/components/Editor/url/extension.ts index 268d5426..5b871760 100644 --- a/src-web/components/Editor/url/extension.ts +++ b/src-web/components/Editor/url/extension.ts @@ -1,43 +1,16 @@ -import { parser } from './url'; -// import { foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language'; -import { styleTags, tags as t } from '@lezer/highlight'; -import { LanguageSupport, LRLanguage } from '@codemirror/language'; import { completeFromList } from '@codemirror/autocomplete'; - -const parserWithMetadata = parser.configure({ - props: [ - styleTags({ - Protocol: t.comment, - Port: t.attributeName, - Host: t.variableName, - PathSegment: t.bool, - Slash: t.bool, - Question: t.attributeName, - QueryName: t.attributeName, - QueryValue: t.attributeName, - Amp: t.comment, - Equal: t.comment, - }), - // indentNodeProp.add({ - // Application: (context) => context.column(context.node.from) + context.unit, - // }), - // foldNodeProp.add({ - // Application: foldInside, - // }), - ], -}); +import { LanguageSupport, LRLanguage } from '@codemirror/language'; +import { parser } from './url'; const urlLanguage = LRLanguage.define({ - parser: parserWithMetadata, - languageData: { - // commentTokens: {line: ";"} - }, + parser, + languageData: {}, }); const exampleCompletion = urlLanguage.data.of({ autocomplete: completeFromList([ - { label: 'http://', type: 'keyword' }, - { label: 'https://', type: 'keyword' }, + { label: 'http://', type: 'constant' }, + { label: 'https://', type: 'constant' }, ]), }); diff --git a/src-web/components/Editor/url/highlight.ts b/src-web/components/Editor/url/highlight.ts new file mode 100644 index 00000000..bedbf058 --- /dev/null +++ b/src-web/components/Editor/url/highlight.ts @@ -0,0 +1,9 @@ +import { styleTags, tags as t } from '@lezer/highlight'; + +export const highlight = styleTags({ + Protocol: t.comment, + Port: t.attributeName, + Host: t.variableName, + Path: t.bool, + Query: t.string, +}); diff --git a/src-web/components/Editor/url/url.grammar b/src-web/components/Editor/url/url.grammar index 432f9867..aa2685cd 100644 --- a/src-web/components/Editor/url/url.grammar +++ b/src-web/components/Editor/url/url.grammar @@ -1,29 +1,18 @@ -@top url { Protocol Host Port? Path Query } - -Protocol { - "http://" | "https://" -} - -Path { - (Slash PathSegment)* -} +@top url { Protocol? Host Port? Path? Query? } Query { - Question (QueryPair)* -} - -QueryPair { - Amp? QueryName Equal QueryValue + "?" queryPair ("&" queryPair)* } @tokens { + Protocol { $[a-zA-Z]+ "://" } + Path { ("/" $[a-zA-Z0-9\-_.]*)+ } + queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) } Port { ":" $[0-9]+ } Host { $[a-zA-Z0-9-_.]+ } - QueryName { $[a-zA-Z0-9-_.]+ } - QueryValue { $[a-zA-Z0-9-_./]+ } - PathSegment { $[a-zA-Z0-9-_.]+ } - Slash { "/" } - Question { "?" } - Equal { "=" } - Amp { "&" } + + // Protocol/host overlaps, so give proto explicit precedence + @precedence { Protocol, Host } } + +@external propSource highlight from "./highlight" diff --git a/src-web/components/Editor/url/url.terms.ts b/src-web/components/Editor/url/url.terms.ts index b7174166..39a59ed2 100644 --- a/src-web/components/Editor/url/url.terms.ts +++ b/src-web/components/Editor/url/url.terms.ts @@ -5,12 +5,4 @@ export const Host = 3, Port = 4, Path = 5, - Slash = 6, - PathSegment = 7, - Query = 8, - Question = 9, - QueryPair = 10, - Amp = 11, - QueryName = 12, - Equal = 13, - QueryValue = 14 + Query = 6 diff --git a/src-web/components/Editor/url/url.ts b/src-web/components/Editor/url/url.ts index 16281422..48df68ad 100644 --- a/src-web/components/Editor/url/url.ts +++ b/src-web/components/Editor/url/url.ts @@ -1,16 +1,18 @@ // This file was generated by lezer-generator. You probably shouldn't edit it. import {LRParser} from "@lezer/lr" +import {highlight} from "./highlight" export const parser = LRParser.deserialize({ version: 14, - states: "#xOQOPOOOOOO'#C^'#C^OYOQOOO_OPOOOjOSO'#CkOoOPO'#CaOwOPOOObOPOOOOOO,59V,59VOOOO-E6i-E6iO|OWO'#CdQOOOOOO!XOPO'#CfO!^OWO'#CfOOOO'#Cl'#ClO!cOWO,59OO!nO`O,59QO!sOPO,59QOOOO-E6j-E6jOOOO1G.l1G.lO!xO`O1G.lOOOO7+$W7+$W", - stateData: "!}~ObPOcPO~ORRO~OSVOUSOXTP~OVWO~OUSOXTX~OXYO~OZ]O[[OaWX~O]`O~O[aO~OZ]O[[OaWa~O^cO~O]dO~O^eO~O", - goto: "}aPPbPPePPiPlPPPPpwRQOTURVRZUT^Y_STRVRXTQ_YRb_", - nodeNames: "⚠ url Protocol Host Port Path Slash PathSegment Query Question QueryPair Amp QueryName Equal QueryValue", - maxTerm: 19, + states: "!jOQOPOOQYOPOOOTOPOOOeOQO'#CbQOOOOOQ`OPOOQ]OPOOOjOPO,58|OrOQO'#CcOwOPO1G.hOOOO,58},58}OOOO-E6a-E6a", + stateData: "!S~OQQORPO~OSUOTTOXRO~OYVO~OZWOWUa~OYYO~OZWOWUi~OQR~", + goto: "dWPPPPPPX^VSPTUQXVRZX", + nodeNames: "⚠ url Protocol Host Port Path Query", + maxTerm: 11, + propSources: [highlight], skippedNodes: [0], - repeatNodeCount: 2, - tokenData: ")]~R]vwz}!O!P!O!P!P!P!Q#]!Q![!P![!]#y!_!`$X!a!b$^!c!}!P#R#S!P#T#[!P#[#]$c#]#o!P~!POZ~n![VRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P!c!}!P#R#S!P#T#o!P`!vV^`}!O!q!O!P!q!P!Q!q!Q![!q!c!}!q#R#S!q#T#o!qa#dV^`UP}!O!q!O!P!q!P!Q!q!Q![!q!c!}!q#R#S!q#T#o!q~#|P!Q![$P~$UPS~!Q![$P~$^O]~~$cOX~o$nXRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P!c!}!P#R#S!P#T#h!P#h#i%Z#i#o!Po%fXRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P!c!}!P#R#S!P#T#h!P#h#i&R#i#o!Po&^XRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P!c!}!P#R#S!P#T#d!P#d#e&y#e#o!Po'UYRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P![!]'t!c!}!P#R#S!P#T#g!P#g#h(V#h#o!PP'wP!P!Q'zP'}P!P!Q(QP(VObPo(bWRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P![!](z!c!}!P#R#S!P#T#o!PP(}P!P!Q)QP)TP!P!Q)WP)]OcP", - tokenizers: [0, 1, 2, 3, 4], + repeatNodeCount: 1, + tokenData: "%[~RYvwq}!Ov!O!Pv!P!Q!_!Q![!y![!]#u!a!b$T!c!}$Y#R#Sv#T#o$Y~vOZ~P{URP}!Ov!O!Pv!Q![v!c!}v#R#Sv#T#ov~!dVT~}!O!_!O!P!_!P!Q!_!Q![!_!c!}!_#R#S!_#T#o!_R#QVYQRP}!Ov!O!Pv!Q![!y!_!`#g!c!}!y#R#Sv#T#o!yQ#lRYQ!Q![#g!c!}#g#T#o#g~#xP!Q![#{~$QPS~!Q![#{~$YOX~R$aWYQRP}!Ov!O!Pv!Q![!y![!]$y!_!`#g!c!}$Y#R#Sv#T#o$YP$|P!P!Q%PP%SP!P!Q%VP%[OQP", + tokenizers: [0, 1], topRules: {"url":[0,1]}, - tokenPrec: 0 + tokenPrec: 47 }) diff --git a/src-web/components/Editor/widgets.ts b/src-web/components/Editor/widgets.ts index b394db94..3225da8f 100644 --- a/src-web/components/Editor/widgets.ts +++ b/src-web/components/Editor/widgets.ts @@ -1,12 +1,5 @@ -import { - Decoration, - DecorationSet, - EditorView, - MatchDecorator, - ViewPlugin, - ViewUpdate, - WidgetType, -} from '@codemirror/view'; +import type { DecorationSet, ViewUpdate } from '@codemirror/view'; +import { Decoration, EditorView, MatchDecorator, ViewPlugin, WidgetType } from '@codemirror/view'; class PlaceholderWidget extends WidgetType { constructor(readonly name: string) { @@ -40,7 +33,7 @@ class BetterMatchDecorator extends MatchDecorator { } const placeholderMatcher = new BetterMatchDecorator({ - regexp: /\{\{\s*([^}\s]+)\s*}}/g, + regexp: /\$\{\[\s*([^\]\s]+)\s*]}/g, decoration(match, view, matchStartPos) { const matchEndPos = matchStartPos + match[0].length - 1; diff --git a/src-web/components/Input.tsx b/src-web/components/Input.tsx index 85626c74..6aa9a85c 100644 --- a/src-web/components/Input.tsx +++ b/src-web/components/Input.tsx @@ -1,4 +1,4 @@ -import { InputHTMLAttributes, ReactNode } from 'react'; +import type { InputHTMLAttributes, ReactNode } from 'react'; import classnames from 'classnames'; import { HStack, VStack } from './Stacks'; import Editor from './Editor/Editor'; @@ -11,6 +11,7 @@ interface Props extends Omit, 'size' | 'on containerClassName?: string; onChange?: (value: string) => void; onSubmit?: () => void; + useTemplating?: boolean; useEditor?: boolean; leftSlot?: ReactNode; rightSlot?: ReactNode; @@ -24,6 +25,7 @@ export function Input({ containerClassName, labelClassName, onSubmit, + useTemplating, size = 'md', useEditor, onChange, @@ -61,6 +63,7 @@ export function Input({ -
+
{response.status} {response.statusReason && ` ${response.statusReason}`}  •  {response.elapsed}ms  •  {Math.round(response.body.length / 1000)} KB
- {contentType.includes('html') && ( - setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))} - /> - )} + +
{response.url}
+ {contentType.includes('html') && ( + setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))} + /> + )} +
{viewMode === 'pretty' && contentType.includes('html') ? (