#424 - Implementation of the snippet wrapper

This commit is contained in:
Elio Struyf
2023-04-18 09:42:15 +02:00
parent d11c7e8509
commit 89344aef15
9 changed files with 178 additions and 47 deletions
+2
View File
@@ -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
+51 -3
View File
@@ -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(`<!-- ${SNIPPET.wrapper.start} data:`, '')
.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);
}
+6
View File
@@ -0,0 +1,6 @@
export const SNIPPET = {
wrapper: {
start: `FM:Snippet:Start`,
end: `FM:Snippet:End`
}
};
@@ -32,7 +32,7 @@ export interface IItemProps {
export const Item: React.FunctionComponent<IItemProps> = ({
snippetKey,
snippet
snippet,
}: React.PropsWithChildren<IItemProps>) => {
const viewData = useRecoilValue(ViewDataSelector);
const settings = useRecoilValue(SettingsSelector);
@@ -143,6 +143,22 @@ export const Item: React.FunctionComponent<IItemProps> = ({
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 (
<>
<li className={`group relative overflow-hidden shadow-md hover:shadow-xl dark:shadow-none border p-4 space-y-2 rounded ${getColors(
@@ -254,7 +270,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({
okBtnText="Insert"
cancelBtnText="Cancel"
>
<SnippetForm ref={formRef} snippet={snippet} selection={viewData?.data?.selection} />
<SnippetForm
ref={formRef}
snippetKey={snippetKey}
snippet={snippet}
fieldInfo={viewData?.data?.snippetInfo?.fields}
selection={viewData?.data?.selection} />
</FormDialog>
)}
@@ -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<SnippetFormHandle, ISnippetFormProps> = (
{ snippet, selection, mediaData, onInsert },
{ snippetKey, snippet, selection, fieldInfo, mediaData, onInsert },
ref
) => {
const viewData = useRecoilValue(ViewDataSelector);
@@ -94,11 +97,26 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
return;
}
const snippetInfo = {
id: snippetKey,
fields: fields.map(f => ({
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: `<!-- ${SNIPPET.wrapper.start} data:${JSON.stringify(snippetInfo)} -->
${snippetBody}
<!-- ${SNIPPET.wrapper.end} -->`
});
}
} else {
onInsert(snippetBody);
}
@@ -165,7 +183,7 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
{field.title || field.name}
</label>
<div className="mt-1">
<SnippetInputField field={field} onValueChange={onTextChange} />
<SnippetInputField field={field} fieldInfo={fieldInfo} onValueChange={onTextChange} />
</div>
</div>
)
@@ -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<ISnippetInputFieldProps> = ({
field,
fieldInfo,
onValueChange
}: React.PropsWithChildren<ISnippetInputFieldProps>) => {
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 (
<div className="relative">
<select
name={field.name}
value={field.value || ''}
className={`block w-full sm:text-sm ${
getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
className={`block w-full sm:text-sm ${getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
onChange={(e) => onValueChange(field, e.target.value)}
>
{(field.choices || [])?.map((option: string | Choice, index: number) =>
@@ -51,12 +62,11 @@ export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps>
<textarea
name={field.name}
value={field.value || ''}
className={`block w-full sm:text-sm h-auto ${
getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
className={`block w-full sm:text-sm h-auto ${getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
onChange={(e) => onValueChange(field, e.currentTarget.value)}
rows={4}
/>
@@ -68,12 +78,11 @@ export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps>
type="text"
name={field.name}
value={field.value || ''}
className={`block w-full sm:text-sm ${
getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
className={`block w-full sm:text-sm ${getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-[var(--vscode-focusBorder)] focus:outline-1 focus:outline-offset-0 focus:shadow-none focus:border-transparent'
)
}`}
onChange={(e) => onValueChange(field, e.currentTarget.value)}
/>
);
@@ -51,8 +51,6 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (
// Contains in key or description, values included in key are ranked higher (sort and fuzzy search)
return keyValue.includes(value) || descriptionValue.includes(value);
});
}, [settings?.snippets, snippetFilter, viewData?.data?.filePath]);
const onSnippetAdd = useCallback(() => {
+25 -13
View File
@@ -1,5 +1,5 @@
import { EditorHelper } from '@estruyf/vscode';
import { window } from 'vscode';
import { window, Range, Position } from 'vscode';
import { Dashboard } from '../../commands/Dashboard';
import { SETTING_CONTENT_SNIPPETS, TelemetryEvent } from '../../constants';
import { DashboardMessage } from '../../dashboardWebView/DashboardMessage';
@@ -80,7 +80,7 @@ export class SnippetListener extends BaseListener {
}
private static async insertSnippet(data: any) {
const { file, snippet } = data;
const { file, snippet, range } = data;
if (!file || !snippet) {
return;
@@ -90,18 +90,30 @@ export class SnippetListener extends BaseListener {
Dashboard.resetViewData();
const editor = window.activeTextEditor;
const position = editor?.selection?.active;
if (!position) {
return;
}
const selection = editor?.selection;
await editor?.edit((builder) => {
if (selection !== undefined) {
builder.replace(selection, snippet);
} else {
builder.insert(position, snippet);
if (range) {
await editor?.edit((builder) => {
const vsCodeRange = new Range(
new Position((range as Range).start.line, (range as Range).start.character),
new Position((range as Range).end.line, (range as Range).end.character)
);
builder.replace(vsCodeRange, snippet);
});
} else {
const position = editor?.selection?.active;
if (!position) {
return;
}
});
const selection = editor?.selection;
await editor?.edit((builder) => {
if (selection !== undefined) {
builder.replace(selection, snippet);
} else {
builder.insert(position, snippet);
}
});
}
}
}
+17
View File
@@ -13,6 +13,8 @@ export interface ViewData {
position?: Position;
fileTitle?: string;
selection?: string;
range?: SnippetRange;
snippetInfo?: SnippetInfo;
pageBundle?: boolean;
metadataInsert?: boolean;
blockData?: BlockFieldData;
@@ -24,3 +26,18 @@ export interface ViewData {
type: 'file' | 'media';
fileExtensions?: string[];
}
export interface SnippetRange {
start: Position;
end: Position;
}
export interface SnippetInfo {
id: string;
fields: SnippetInfoField[];
}
export interface SnippetInfoField {
name: string;
value: string;
}