From 3f516ef012317845ecc3c82bab6e30c36ff44b99 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 17 Sep 2024 20:20:56 -0700 Subject: [PATCH 1/3] Support namespaced template functions in parser and autocomplete --- src-tauri/yaak_templates/src/parser.rs | 45 ++++++++++++- src-web/components/core/Editor/Editor.css | 9 +-- .../components/core/Editor/twig/completion.ts | 65 ++++++++++++++----- .../core/Editor/twig/templateTags.ts | 14 ++-- src-web/hooks/useRenderTemplate.ts | 1 + 5 files changed, 108 insertions(+), 26 deletions(-) diff --git a/src-tauri/yaak_templates/src/parser.rs b/src-tauri/yaak_templates/src/parser.rs index e2565570..1603ac31 100644 --- a/src-tauri/yaak_templates/src/parser.rs +++ b/src-tauri/yaak_templates/src/parser.rs @@ -195,7 +195,7 @@ impl Parser { fn parse_fn(&mut self) -> Option<(String, Vec)> { let start_pos = self.pos; - let name = match self.parse_ident() { + let name = match self.parse_fn_name() { Some(v) => v, None => { self.pos = start_pos; @@ -292,6 +292,32 @@ impl Parser { Some(text) } + + fn parse_fn_name(&mut self) -> Option { + let start_pos = self.pos; + + let mut text = String::new(); + while self.pos < self.chars.len() { + let ch = self.peek_char(); + if ch.is_alphanumeric() || ch == '_' || ch == '.' { + text.push(ch); + self.pos += 1; + } else { + break; + } + + if start_pos == self.pos { + panic!("Parser stuck!"); + } + } + + if text.is_empty() { + self.pos = start_pos; + return None; + } + + Some(text) + } fn parse_string(&mut self) -> Option { let start_pos = self.pos; @@ -486,6 +512,23 @@ mod tests { ] ); } + + #[test] + fn fn_dot_name() { + let mut p = Parser::new("${[ foo.bar.baz() ]}"); + assert_eq!( + p.parse().tokens, + vec![ + Token::Tag { + val: Val::Fn { + name: "foo.bar.baz".into(), + args: Vec::new(), + } + }, + Token::Eof + ] + ); + } #[test] fn fn_ident_arg() { diff --git a/src-web/components/core/Editor/Editor.css b/src-web/components/core/Editor/Editor.css index 32a792cc..87c4a883 100644 --- a/src-web/components/core/Editor/Editor.css +++ b/src-web/components/core/Editor/Editor.css @@ -242,6 +242,11 @@ @apply text-primary; } + &.cm-completionIcon-namespace::after { + content: 'n' !important; + @apply text-warning; + } + &.cm-completionIcon-constant::after { content: 'c' !important; @apply text-notice; @@ -267,10 +272,6 @@ content: 'm' !important; } - &.cm-completionIcon-namespace::after { - content: 'n' !important; - } - &.cm-completionIcon-property::after { content: 'a' !important; } diff --git a/src-web/components/core/Editor/twig/completion.ts b/src-web/components/core/Editor/twig/completion.ts index 6511a846..013df648 100644 --- a/src-web/components/core/Editor/twig/completion.ts +++ b/src-web/components/core/Editor/twig/completion.ts @@ -1,4 +1,4 @@ -import type { CompletionContext } from '@codemirror/autocomplete'; +import type { Completion, CompletionContext } from '@codemirror/autocomplete'; const openTag = '${[ '; const closeTag = ' ]}'; @@ -7,12 +7,20 @@ export type TwigCompletionOptionVariable = { type: 'variable'; }; +export type TwigCompletionOptionNamespace = { + type: 'namespace'; +}; + export type TwigCompletionOptionFunction = { args: { name: string }[]; type: 'function'; }; -export type TwigCompletionOption = (TwigCompletionOptionFunction | TwigCompletionOptionVariable) & { +export type TwigCompletionOption = ( + | TwigCompletionOptionFunction + | TwigCompletionOptionVariable + | TwigCompletionOptionNamespace +) & { name: string; label: string; onClick: (rawTag: string, startPos: number) => void; @@ -25,12 +33,13 @@ export interface TwigCompletionConfig { } const MIN_MATCH_VAR = 1; -const MIN_MATCH_NAME = 2; +const MIN_MATCH_NAME = 1; export function twigCompletion({ options }: TwigCompletionConfig) { return function completions(context: CompletionContext) { - const toStartOfName = context.matchBefore(/\w*/); - const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*\w*/); + const toStartOfName = context.matchBefore(/[\w_.]*/); + const toStartOfNamespacedName = context.matchBefore(/[\w_.]*/); + const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*[\w_]*/); const toMatch = toStartOfVariable ?? toStartOfName ?? null; if (toMatch === null) return null; @@ -47,22 +56,46 @@ export function twigCompletion({ options }: TwigCompletionConfig) { return null; } + const completions: Completion[] = options + .map((o): Completion => { + const optionSegments = o.name.split('.'); + const matchSegments = toStartOfNamespacedName!.text.split('.'); + // const prefix = toStartOfNamespacedName!.text + // .split('.') + // .slice(0, matchSegments.length) + // .join('.'); + + // // Remove anything that doesn't start with the prefixed text + // if (!o.name.startsWith(prefix)) { + // return null; + // } + + // If not on the last segment, only complete the namespace + if (matchSegments.length < optionSegments.length) { + return { + label: optionSegments.slice(0, matchSegments.length).join('.'), + apply: optionSegments.slice(0, matchSegments.length).join('.'), + type: 'namespace', + }; + } + + // If on the last segment, wrap the entire tag + const inner = o.type === 'function' ? `${o.name}()` : o.name; + return { + label: o.name, + apply: openTag + inner + closeTag, + type: o.type === 'variable' ? 'variable' : 'function', + }; + }) + .filter((v) => v != 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 { validFor: () => true, // Not really sure why this is all it needs from: toMatch.from, - options: options - .filter((v) => v.name.trim()) - .map((v) => { - const inner = v.type === 'function' ? `${v.name}()` : v.name; - return { - label: v.label, - apply: openTag + inner + closeTag, - type: v.type === 'variable' ? 'variable' : 'function', - matchLen: matchLen, - }; - }) + matchLen, + options: completions // Filter out exact matches .filter((o) => o.label !== toMatch.text), }; diff --git a/src-web/components/core/Editor/twig/templateTags.ts b/src-web/components/core/Editor/twig/templateTags.ts index 962b441a..f949fb6e 100644 --- a/src-web/components/core/Editor/twig/templateTags.ts +++ b/src-web/components/core/Editor/twig/templateTags.ts @@ -9,7 +9,11 @@ import type { TwigCompletionOption } from './completion'; class PathPlaceholderWidget extends WidgetType { readonly #clickListenerCallback: () => void; - constructor(readonly rawText: string, readonly startPos: number, readonly onClick: () => void) { + constructor( + readonly rawText: string, + readonly startPos: number, + readonly onClick: () => void, + ) { super(); this.#clickListenerCallback = () => { this.onClick?.(); @@ -68,10 +72,10 @@ class TemplateTagWidget extends WidgetType { this.option.invalid ? 'x-theme-templateTag--danger' : this.option.type === 'variable' - ? 'x-theme-templateTag--primary' - : 'x-theme-templateTag--info' + ? 'x-theme-templateTag--primary' + : 'x-theme-templateTag--info' }`; - elt.title = this.option.invalid ? 'Not Found' : this.option.value ?? ''; + elt.title = this.option.invalid ? 'Not Found' : (this.option.value ?? ''); elt.setAttribute('data-tag-type', this.option.type); elt.textContent = this.option.type === 'variable' @@ -134,7 +138,7 @@ function templateTags( // TODO: Search `node.tree` instead of using Regex here const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, ''); - let name = inner.match(/(\w+)[(]/)?.[1] ?? inner; + let name = inner.match(/([\w.]+)[(]/)?.[1] ?? inner; // The beta named the function `Response` but was changed in stable. // Keep this here for a while because there's no easy way to migrate diff --git a/src-web/hooks/useRenderTemplate.ts b/src-web/hooks/useRenderTemplate.ts index bb401252..f438c91d 100644 --- a/src-web/hooks/useRenderTemplate.ts +++ b/src-web/hooks/useRenderTemplate.ts @@ -8,6 +8,7 @@ export function useRenderTemplate(template: string) { const environmentId = useActiveEnvironment()[0]?.id ?? null; return useQuery({ placeholderData: (prev) => prev, // Keep previous data on refetch + refetchOnWindowFocus: false, queryKey: ['render_template', template], queryFn: () => renderTemplate({ template, workspaceId, environmentId }), }); From 9f5f53a71cf5c038c70ca4f749bfc21a3a4f4c66 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 17 Sep 2024 20:35:18 -0700 Subject: [PATCH 2/3] Remove commented code --- src-web/components/core/Editor/twig/completion.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src-web/components/core/Editor/twig/completion.ts b/src-web/components/core/Editor/twig/completion.ts index 013df648..f37b5676 100644 --- a/src-web/components/core/Editor/twig/completion.ts +++ b/src-web/components/core/Editor/twig/completion.ts @@ -58,17 +58,8 @@ export function twigCompletion({ options }: TwigCompletionConfig) { const completions: Completion[] = options .map((o): Completion => { - const optionSegments = o.name.split('.'); const matchSegments = toStartOfNamespacedName!.text.split('.'); - // const prefix = toStartOfNamespacedName!.text - // .split('.') - // .slice(0, matchSegments.length) - // .join('.'); - - // // Remove anything that doesn't start with the prefixed text - // if (!o.name.startsWith(prefix)) { - // return null; - // } + const optionSegments = o.name.split('.'); // If not on the last segment, only complete the namespace if (matchSegments.length < optionSegments.length) { From 92d11d4d6132e55921dc34879a165ec7a6bdd27b Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 18 Sep 2024 05:32:17 -0700 Subject: [PATCH 3/3] Remove duplicate var --- src-web/components/core/Editor/twig/completion.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src-web/components/core/Editor/twig/completion.ts b/src-web/components/core/Editor/twig/completion.ts index f37b5676..486d65ae 100644 --- a/src-web/components/core/Editor/twig/completion.ts +++ b/src-web/components/core/Editor/twig/completion.ts @@ -38,7 +38,6 @@ const MIN_MATCH_NAME = 1; export function twigCompletion({ options }: TwigCompletionConfig) { return function completions(context: CompletionContext) { const toStartOfName = context.matchBefore(/[\w_.]*/); - const toStartOfNamespacedName = context.matchBefore(/[\w_.]*/); const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*[\w_]*/); const toMatch = toStartOfVariable ?? toStartOfName ?? null; @@ -58,7 +57,7 @@ export function twigCompletion({ options }: TwigCompletionConfig) { const completions: Completion[] = options .map((o): Completion => { - const matchSegments = toStartOfNamespacedName!.text.split('.'); + const matchSegments = toStartOfName!.text.split('.'); const optionSegments = o.name.split('.'); // If not on the last segment, only complete the namespace