mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-08 01:42:58 +02:00
#424 - Implementation of the snippet wrapper
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user