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!QQP)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') ? (