diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3a3bdb..96a2ecdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### ✨ New features +- [#424](https://github.com/estruyf/vscode-front-matter/issues/424): Snippet wrapping to allow easier updates or changes to previously set snippets in the content + ### 🎨 Enhancements - [#566](https://github.com/estruyf/vscode-front-matter/issues/566): Keep the panel context on the live preview diff --git a/src/commands/Article.ts b/src/commands/Article.ts index 8d03c1d7..e1a5e0f5 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -19,7 +19,7 @@ import { ArticleHelper, Settings, SlugHelper, TaxonomyHelper } from '../helpers' import { Notifications } from '../helpers/Notifications'; import { extname, basename, parse, dirname } from 'path'; import { COMMAND_NAME, DefaultFields } from '../constants'; -import { DashboardData } from '../models/DashboardData'; +import { DashboardData, SnippetRange } from '../models/DashboardData'; import { DateHelper } from '../helpers/DateHelper'; import { parseWinPath } from '../helpers/parseWinPath'; import { Telemetry } from '../helpers/Telemetry'; @@ -27,6 +27,8 @@ import { ParsedFrontMatter } from '../parsers'; import { MediaListener } from '../listeners/panel'; import { NavigationType } from '../dashboardWebView/models'; import { processKnownPlaceholders } from '../helpers/PlaceholderHelper'; +import { Position } from 'vscode'; +import { SNIPPET } from '../constants/Snippet'; export class Article { /** @@ -418,9 +420,53 @@ export class Article { return; } - const position = editor.selection.active; + let position = editor.selection.active; const selectionText = editor.document.getText(editor.selection); + // Check for snippet wrapper + const selectionStart = editor.selection.start; + const docText = editor.document.getText(); + const docTextLines = docText.split(`\n`); + const snippetEndAfterPos = docTextLines.findIndex((value: string, idx: number) => { + return value.includes(SNIPPET.wrapper.end) && idx >= selectionStart.line; + }); + + const snippetStartAfterPos = docTextLines.findIndex((value: string, idx: number) => { + return value.includes(SNIPPET.wrapper.start) && idx > selectionStart.line; + }); + + const linesBeforeSelection = docTextLines.slice(0, selectionStart.line + 1); + + let snippetStartBeforePos = linesBeforeSelection + .reverse() + .findIndex((r) => r.includes(SNIPPET.wrapper.start)); + + if (snippetStartBeforePos > -1) { + snippetStartBeforePos = linesBeforeSelection.length - snippetStartBeforePos - 1; + } + + let snippetInfo: { id: string; fields: any[] } | undefined = undefined; + let range: SnippetRange | undefined = undefined; + if ( + (snippetStartAfterPos > snippetEndAfterPos || snippetStartAfterPos === -1) && + snippetStartBeforePos + ) { + // Content was within a snippet block, get all the text + const snippetBlock = docTextLines.slice(snippetStartBeforePos, snippetEndAfterPos + 1); + const firstLine = snippetBlock[0]; + + range = { + start: new Position(snippetStartBeforePos, 0), + end: new Position(snippetEndAfterPos, snippetBlock[snippetBlock.length - 1].length) + }; + + const data = firstLine + .replace(`', '') + .replace("'", '"'); + snippetInfo = JSON.parse(data); + } + const article = ArticleHelper.getFrontMatter(editor); await vscode.commands.executeCommand(COMMAND_NAME.dashboard, { @@ -430,7 +476,9 @@ export class Article { filePath: editor.document.uri.fsPath, fieldName: basename(editor.document.uri.fsPath), position, - selection: selectionText + range, + selection: selectionText, + snippetInfo } } as DashboardData); } diff --git a/src/constants/Snippet.ts b/src/constants/Snippet.ts new file mode 100644 index 00000000..97019db8 --- /dev/null +++ b/src/constants/Snippet.ts @@ -0,0 +1,6 @@ +export const SNIPPET = { + wrapper: { + start: `FM:Snippet:Start`, + end: `FM:Snippet:End` + } +}; diff --git a/src/dashboardWebView/components/SnippetsView/Item.tsx b/src/dashboardWebView/components/SnippetsView/Item.tsx index e96327e9..15dc307e 100644 --- a/src/dashboardWebView/components/SnippetsView/Item.tsx +++ b/src/dashboardWebView/components/SnippetsView/Item.tsx @@ -32,7 +32,7 @@ export interface IItemProps { export const Item: React.FunctionComponent = ({ snippetKey, - snippet + snippet, }: React.PropsWithChildren) => { const viewData = useRecoilValue(ViewDataSelector); const settings = useRecoilValue(SettingsSelector); @@ -143,6 +143,22 @@ export const Item: React.FunctionComponent = ({ setShowAlert(false); }, [settings?.snippets, snippetKey]); + React.useEffect(() => { + if (viewData?.data?.snippetInfo?.id && snippetKey && viewData.data.snippetInfo.id === snippetKey) { + if (snippet) { + setSnippetTitle(snippet.title || viewData?.data?.snippetInfo?.id); + setSnippetDescription(snippet.description); + setSnippetOriginalBody( + typeof snippet.body === 'string' + ? snippet.body + : snippet.body.join(`\n`) + ); + setMediaSnippet(!!snippet.isMediaSnippet); + setShowInsertDialog(true); + } + } + }, [viewData?.data?.snippetInfo?.id, snippetKey, snippet]); + return ( <>
  • = ({ okBtnText="Insert" cancelBtnText="Cancel" > - + )} diff --git a/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx index 01fd5fac..9095cef6 100644 --- a/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx +++ b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx @@ -4,15 +4,18 @@ import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from ' import { useRecoilValue } from 'recoil'; import { processKnownPlaceholders } from '../../../helpers/PlaceholderHelper'; import { SnippetParser } from '../../../helpers/SnippetParser'; -import { Snippet, SnippetField, SnippetSpecialPlaceholders } from '../../../models'; +import { Snippet, SnippetField, SnippetInfoField, SnippetSpecialPlaceholders } from '../../../models'; import { DashboardMessage } from '../../DashboardMessage'; import useThemeColors from '../../hooks/useThemeColors'; import { SettingsAtom, ViewDataSelector } from '../../state'; import { SnippetInputField } from './SnippetInputField'; +import { SNIPPET } from '../../../constants/Snippet'; export interface ISnippetFormProps { + snippetKey?: string; snippet: Snippet; selection: string | undefined; + fieldInfo?: SnippetInfoField[]; mediaData?: any; onInsert?: (mediaData: any) => void; } @@ -22,7 +25,7 @@ export interface SnippetFormHandle { } const SnippetForm: React.ForwardRefRenderFunction = ( - { snippet, selection, mediaData, onInsert }, + { snippetKey, snippet, selection, fieldInfo, mediaData, onInsert }, ref ) => { const viewData = useRecoilValue(ViewDataSelector); @@ -94,11 +97,26 @@ const SnippetForm: React.ForwardRefRenderFunction ({ + name: f.name, + value: f.value + })) + } + if (!onInsert) { - Messenger.send(DashboardMessage.insertSnippet, { - file: viewData?.data?.filePath, - snippet: snippetBody - }); + if (!snippetKey) { + Messenger.send(DashboardMessage.insertSnippet, snippetBody); + } else { + Messenger.send(DashboardMessage.insertSnippet, { + file: viewData?.data?.filePath, + range: viewData?.data?.range, + snippet: ` +${snippetBody} +` + }); + } } else { onInsert(snippetBody); } @@ -165,7 +183,7 @@ const SnippetForm: React.ForwardRefRenderFunction
    - +
    ) diff --git a/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx b/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx index b699d266..293fe591 100644 --- a/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx +++ b/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx @@ -1,31 +1,42 @@ import * as React from 'react'; import { ChevronDownIcon } from '@heroicons/react/outline'; -import { Choice, SnippetField } from '../../../models'; +import { Choice, SnippetField, SnippetInfoField } from '../../../models'; import useThemeColors from '../../hooks/useThemeColors'; +import { useEffect } from 'react'; export interface ISnippetInputFieldProps { field: SnippetField; + fieldInfo?: SnippetInfoField[]; onValueChange: (field: SnippetField, value: string) => void; } export const SnippetInputField: React.FunctionComponent = ({ field, + fieldInfo, onValueChange }: React.PropsWithChildren) => { const { getColors } = useThemeColors(); - + + useEffect(() => { + if (fieldInfo) { + const info = fieldInfo.find((f) => f.name === field.name); + if (info) { + onValueChange(field, info.value || ''); + } + } + }, [fieldInfo]); + if (field.type === 'choice') { return (