diff --git a/.run/Dev Desktop.run.xml b/.run/Dev Desktop.run.xml index 4ebc595c..5fbed34e 100644 --- a/.run/Dev Desktop.run.xml +++ b/.run/Dev Desktop.run.xml @@ -7,8 +7,8 @@ - + - \ No newline at end of file + diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 91eb085b..90b6acd0 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index a7181475..4e534b32 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index 349717c8..179779b3 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 38a8f582..706f5fcc 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index 0c600807..e8126a6b 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 677b2fc7..0b4e7bba 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index 52442448..e2c53f7f 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 7a0a1ea1..1b05c23c 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index 071db292..46691e23 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index ac214202..b939e4aa 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 2ab8447c..29be51b3 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index 1b4ebe32..c88253a3 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 6046af1f..13683473 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index cb6a4161..2c7bea99 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index e94bcf6b..ccd1ad36 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index 0eeac1eb..c60732b5 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/tauri.toml b/src-tauri/tauri.toml index 39e5d3d9..616e6268 100644 --- a/src-tauri/tauri.toml +++ b/src-tauri/tauri.toml @@ -6,7 +6,7 @@ distDir = "../dist" withGlobalTauri = false [package] -productName = "Twosomnia" +productName = "Yaak" version = "0.0.1" [tauri.allowlist] @@ -34,7 +34,7 @@ icon = [ "icons/icon.icns", "icons/icon.ico" ] -identifier = "co.schier.twosomnia" +identifier = "co.schier.yaak" longDescription = "" resources = [ "plugins/*", "migrations/*" ] shortDescription = "" @@ -63,7 +63,7 @@ dialog = true fullscreen = false height = 800 resizable = true -title = "Twosomnia" +title = "Yaak" width = 1_400 titleBarStyle = "Overlay" hiddenTitle = true diff --git a/src-web/components/Editor/Editor.css b/src-web/components/Editor/Editor.css index 00dd6a95..f2d9f684 100644 --- a/src-web/components/Editor/Editor.css +++ b/src-web/components/Editor/Editor.css @@ -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; +} diff --git a/src-web/components/Editor/Editor.tsx b/src-web/components/Editor/Editor.tsx index 33cc0e95..b57a0119 100644 --- a/src-web/components/Editor/Editor.tsx +++ b/src-web/components/Editor/Editor.tsx @@ -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, '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()); diff --git a/src-web/components/Editor/completion/completion.ts b/src-web/components/Editor/completion/completion.ts index 32c4cd1e..b073d9b3 100644 --- a/src-web/components/Editor/completion/completion.ts +++ b/src-web/components/Editor/completion/completion.ts @@ -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', })), }; diff --git a/src-web/components/Editor/extensions.ts b/src-web/components/Editor/extensions.ts index bd25dcba..43fa55a5 100644 --- a/src-web/components/Editor/extensions.ts +++ b/src-web/components/Editor/extensions.ts @@ -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), ]; diff --git a/src-web/components/Editor/singleLine.ts b/src-web/components/Editor/singleLine.ts new file mode 100644 index 00000000..1a166b59 --- /dev/null +++ b/src-web/components/Editor/singleLine.ts @@ -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; + }, + ); +} diff --git a/src-web/components/Editor/twig/extension.ts b/src-web/components/Editor/twig/extension.ts index 43cac83e..b0d226f1 100644 --- a/src-web/components/Editor/twig/extension.ts +++ b/src-web/components/Editor/twig/extension.ts @@ -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 }); } diff --git a/src-web/components/Editor/twig/twig.grammar b/src-web/components/Editor/twig/twig.grammar index 60b1591d..11324af9 100644 --- a/src-web/components/Editor/twig/twig.grammar +++ b/src-web/components/Editor/twig/twig.grammar @@ -9,9 +9,9 @@ directive { } @tokens { - Text { ![${[] Text? } + Text { ![$] Text? } space { @whitespace+ } - DirectiveContent { ![\]}$] DirectiveContent? } + DirectiveContent { ![\]}] DirectiveContent? } @precedence { space DirectiveContent } "${[" "]}" } diff --git a/src-web/components/Editor/twig/twig.ts b/src-web/components/Editor/twig/twig.ts index 3057bdaa..a806f562 100644 --- a/src-web/components/Editor/twig/twig.ts +++ b/src-web/components/Editor/twig/twig.ts @@ -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 diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 524a70c9..210427e0 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -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 ? : <>, onSelect: () => setActiveResponseId(r.id), })),