diff --git a/package-lock.json b/package-lock.json index 8c0fa19b..cf5cc602 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/lodash.uniqby": "4.7.6", "@types/lodash.xor": "^4.5.6", "@types/mocha": "^5.2.6", + "@types/mustache": "^4.1.2", "@types/node": "10.17.48", "@types/node-fetch": "^2.5.12", "@types/react": "17.0.0", @@ -56,6 +57,7 @@ "lodash.uniqby": "4.7.0", "lodash.xor": "^4.5.0", "mdast-util-from-markdown": "1.0.0", + "mustache": "^4.2.0", "node-json-db": "^1.3.0", "npm-run-all": "^4.1.5", "path-browserify": "^1.0.1", @@ -822,6 +824,12 @@ "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", "dev": true }, + "node_modules/@types/mustache": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.1.2.tgz", + "integrity": "sha512-c4OVMMcyodKQ9dpwBwh3ofK9P6U9ZktKU9S+p33UqwMNN1vlv2P0zJZUScTshnx7OEoIIRcCFNQ904sYxZz8kg==", + "dev": true + }, "node_modules/@types/node": { "version": "10.17.48", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.48.tgz", @@ -5986,6 +5994,15 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", "dev": true }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/nanoid": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", @@ -9932,6 +9949,12 @@ "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", "dev": true }, + "@types/mustache": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.1.2.tgz", + "integrity": "sha512-c4OVMMcyodKQ9dpwBwh3ofK9P6U9ZktKU9S+p33UqwMNN1vlv2P0zJZUScTshnx7OEoIIRcCFNQ904sYxZz8kg==", + "dev": true + }, "@types/node": { "version": "10.17.48", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.48.tgz", @@ -13835,6 +13858,12 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", "dev": true }, + "mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true + }, "nanoid": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", diff --git a/package.json b/package.json index d8331f7e..d7e8ea7d 100644 --- a/package.json +++ b/package.json @@ -225,7 +225,8 @@ "additionalProperties": { "type": "object", "required": [ - "body" + "body", + "fields" ], "properties": { "body": { @@ -241,6 +242,9 @@ "description": { "description": "The snippet description.", "type": "string" + }, + "fields": { + "$ref": "#contenttypefield" } }, "additionalProperties": false @@ -1613,6 +1617,7 @@ "@types/lodash.uniqby": "4.7.6", "@types/lodash.xor": "^4.5.6", "@types/mocha": "^5.2.6", + "@types/mustache": "^4.1.2", "@types/node": "10.17.48", "@types/node-fetch": "^2.5.12", "@types/react": "17.0.0", @@ -1641,6 +1646,7 @@ "lodash.uniqby": "4.7.0", "lodash.xor": "^4.5.0", "mdast-util-from-markdown": "1.0.0", + "mustache": "^4.2.0", "node-json-db": "^1.3.0", "npm-run-all": "^4.1.5", "path-browserify": "^1.0.1", diff --git a/src/constants/SnippetVariables.ts b/src/constants/SnippetVariables.ts deleted file mode 100644 index 5cdc2f2c..00000000 --- a/src/constants/SnippetVariables.ts +++ /dev/null @@ -1,7 +0,0 @@ - - -export const SnippetVariables = { - FM_SELECTED_TEXT: 'FM_SELECTED_TEXT', - FM_TEXT: 'FM_TEXT_', - FM_MULTILINE: 'FM_MULTILINE_', -}; \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index 4c6ae167..ecfbfde9 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -7,7 +7,6 @@ export * from './FrameworkDetectors'; export * from './Links'; export * from './LocalStore'; export * from './Navigation'; -export * from './SnippetVariables'; export * from './TelemetryEvent'; export * from './charCode'; export * from './charMap'; diff --git a/src/dashboardWebView/components/SnippetsView/Item.tsx b/src/dashboardWebView/components/SnippetsView/Item.tsx index db964fc5..c9a27478 100644 --- a/src/dashboardWebView/components/SnippetsView/Item.tsx +++ b/src/dashboardWebView/components/SnippetsView/Item.tsx @@ -3,6 +3,8 @@ import { CodeIcon, DotsHorizontalIcon, PencilIcon, PlusIcon, TrashIcon } from '@ import * as React from 'react'; import { useCallback, useRef, useState } from 'react'; import { useRecoilValue } from 'recoil'; +import { SnippetParser } from '../../../helpers/SnippetParser'; +import { Snippet, SnippetField, Snippets } from '../../../models'; import { DashboardMessage } from '../../DashboardMessage'; import { SettingsSelector, ViewDataSelector } from '../../state'; import { Alert } from '../Modals/Alert'; @@ -12,7 +14,7 @@ import SnippetForm, { SnippetFormHandle } from './SnippetForm'; export interface IItemProps { title: string; - snippet: any; + snippet: Snippet; } export const Item: React.FunctionComponent = ({ title, snippet }: React.PropsWithChildren) => { @@ -53,13 +55,21 @@ export const Item: React.FunctionComponent = ({ title, snippet }: Re return; } - const snippets = Object.assign({}, settings?.snippets || {}); + const snippets: Snippets = Object.assign({}, settings?.snippets || {}); const snippetLines = snippetOriginalBody.split("\n"); - const snippetContents = { + + const crntSnippet = Object.assign({}, snippets[title]); + + const fields = SnippetParser.getFields(snippetLines, crntSnippet.fields || [], crntSnippet?.openingTags, crntSnippet?.closingTags); + + const snippetContents: Snippet = { + ...crntSnippet, + fields, description: snippetDescription || '', body: snippetLines.length === 1 ? snippetLines[0] : snippetLines }; + // Check if new or update if (title === snippetTitle) { snippets[title] = snippetContents; } else { diff --git a/src/dashboardWebView/components/SnippetsView/NewForm.tsx b/src/dashboardWebView/components/SnippetsView/NewForm.tsx index 6d866231..bd2185d1 100644 --- a/src/dashboardWebView/components/SnippetsView/NewForm.tsx +++ b/src/dashboardWebView/components/SnippetsView/NewForm.tsx @@ -1,6 +1,4 @@ -import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/outline'; import * as React from 'react'; -import { SnippetVariables } from '../../../constants'; export interface INewFormProps { title: string; @@ -13,7 +11,6 @@ export interface INewFormProps { } export const NewForm: React.FunctionComponent = ({ title, description, body, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren) => { - const [ showDetails, setShowDetails ] = React.useState(false); return (
@@ -63,47 +60,6 @@ export const NewForm: React.FunctionComponent = ({ title, descrip />
- -
-

- Placeholders guidelines - - { - showDetails ? ( - - ) : ( - - ) - } -

- - -
-
-
Insert selected text (can still be updated)
-
{`\${${SnippetVariables.FM_SELECTED_TEXT}}`}
-
- -
-
Variable without default
-
{`\${variable}`}
-
- -
-
Variable with default
-
{`\${variable:default}`}
-
- -
-
Variable with choices
-
{`\${variable|choice 1,choice 2,choice 3|}`}
-
-
-
); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx index f476f471..fbac028b 100644 --- a/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx +++ b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx @@ -2,16 +2,15 @@ import { Messenger } from '@estruyf/vscode/dist/client'; import * as React from 'react'; import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { SnippetVariables } from '../../../constants'; -import { Choice, SnippetParser, Variable, VariableResolver } from '../../../helpers/SnippetParser'; -import { SnippetField } from '../../../models'; +import { SnippetParser } from '../../../helpers/SnippetParser'; +import { Snippet, SnippetField, SnippetSpecialPlaceholders } from '../../../models'; import { DashboardMessage } from '../../DashboardMessage'; import { ViewDataSelector } from '../../state'; import { SnippetInputField } from './SnippetInputField'; export interface ISnippetFormProps { - snippet: any; + snippet: Snippet; selection: string | undefined; } @@ -27,23 +26,24 @@ const SnippetForm: React.ForwardRefRenderFunction prevFields.map(f => f.name === field.name ? { ...f, value } : f)); }, [setFields]); - const insertSelectionValue = useCallback((value: string) => { - if (selection && value === SnippetVariables.FM_SELECTED_TEXT) { - return selection; + const insertSelectionValue = useCallback((value: SnippetSpecialPlaceholders) => { + if (value === "FM_SELECTED_TEXT") { + return selection || ""; } - return; + return value; }, [selection]); const snippetBody = useMemo(() => { let body = typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`); - + + const obj: any = {}; for (const field of fields) { - body = body.replace(field.tmString, field.value); + obj[field.name] = field.value; } - return body; - }, [fields, selection]); + return SnippetParser.render(body, obj, snippet.openingTags, snippet.closingTags); + }, [fields, snippet]); const shouldShowField = (fieldName: string, idx: number, allFields: SnippetField[]) => { const crntField = allFields.findIndex(f => f.name === fieldName); @@ -53,22 +53,6 @@ const SnippetForm: React.ForwardRefRenderFunction { - if (fieldName === SnippetVariables.FM_SELECTED_TEXT) { - return 'Selected Text'; - } - - if (fieldName.startsWith(SnippetVariables.FM_TEXT)) { - return fieldName.replace(SnippetVariables.FM_TEXT, ''); - } - - if (fieldName.startsWith(SnippetVariables.FM_MULTILINE)) { - return fieldName.replace(SnippetVariables.FM_MULTILINE, ''); - } - - return fieldName; - } - useImperativeHandle(ref, () => ({ onSave() { if (!snippetBody) { @@ -83,62 +67,35 @@ const SnippetForm: React.ForwardRefRenderFunction { - // Defines the type of field that needs to be rendered - const getFieldType = (fieldName: string) => { - if (fieldName.startsWith(SnippetVariables.FM_MULTILINE)) { - return 'textarea'; - } - - return 'text'; - } - // Get all placeholder variables from the snippet - const snippetParser = new SnippetParser(); const body = typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`); - const parsed = snippetParser.parse(body); - const placeholders = parsed.placeholderInfo.all; + const placeholders = SnippetParser.getPlaceholders(body, snippet.openingTags, snippet.closingTags); - const allFields: any[] = []; + const allFields: SnippetField[] = []; + const snippetFields = snippet.fields || []; - for (const placeholder of placeholders) { - const tmString = placeholder.toTextmateString(); + for (const fieldName of placeholders) { + const field = snippetFields.find(f => f.name === fieldName); - // If only a variable is defined, it will not contain children - if (placeholder.children.length === 0) { + if (field) { allFields.push({ - type: getFieldType(placeholder.index as string), - name: placeholder.index, - value: insertSelectionValue(placeholder.index as string) || '', - tmString + ...field, + value: insertSelectionValue(field.default || "") }); } else { - // Children are defined, so it means it is a choice field or the variable has a default value - for (const child of placeholder.children as any[]) { - if (child instanceof Choice) { - const options = child.options.map(o => o.value); - - allFields.push({ - type: 'select', - name: placeholder.index, - value: (child as any).value || options[0] || "", - options, - tmString - }); - } else { - allFields.push({ - type: getFieldType(placeholder.index as string), - name: placeholder.index, - value: insertSelectionValue((child as any).value as string) || insertSelectionValue(placeholder.index as string) || (child as any).value || "", - tmString - }); - } - } + allFields.push({ + name: fieldName, + title: fieldName, + type: "string", + single: true, + value: "" + }); } } setFields(allFields); - }, []); + }, [snippet]); return (
@@ -152,7 +109,7 @@ const SnippetForm: React.ForwardRefRenderFunction
= ({ field, onValueChange }: React.PropsWithChildren) => { - if (field.type === 'select') { + if (field.type === 'choice') { return (
@@ -30,7 +31,7 @@ export const SnippetInputField: React.FunctionComponent ) } - if (field.type === 'textarea') { + if (field.type === 'string' && !field.single) { return (