From 25d14e1c333847cf02b38dea68906c0c57be02a1 Mon Sep 17 00:00:00 2001 From: Luke Murray Date: Thu, 3 Jun 2021 10:22:35 -0400 Subject: [PATCH] end to end works but need to parameterize --- example/index.tsx | 8 ++-- src/SnippetSession.tsx | 49 ++++++++++++++----- src/components/PlaceholderDecoration.tsx | 42 +++++++++++++++++ src/components/SnippetReadonlyText.tsx | 33 +++++++++++++ src/customTypes.ts | 14 +++++- src/slateHelpers.tsx | 8 ++-- src/slateSnippetsExtension.tsx | 60 ++++++++++++++---------- src/snippetParser/snippetParser.ts | 13 ++++- 8 files changed, 182 insertions(+), 45 deletions(-) create mode 100644 src/components/PlaceholderDecoration.tsx create mode 100644 src/components/SnippetReadonlyText.tsx diff --git a/example/index.tsx b/example/index.tsx index c2c8746..51a7c95 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -60,15 +60,15 @@ const App = () => { "description": "You can add your own variable resolutions!" }, "Insert Multiple Tab Stops": { - "prefix": "tabstops", + "prefix": "tab", "body": "This $1 snippet $2 contains multiple tab $3 stops" }, "Insert Placeholders in any order": { - "prefix": "placeholders", - "body": "This \${2:snippet} \${1:contains} multiple \${3:placeholders}" + "prefix": "phs", + "body": "This \${1:snippet} \${2:contains}" }, "Insert multiple lines": { - "prefix": "placeholders", + "prefix": "mls", "body": ["this snippet $0", "\${1:contains} a couple", "lines of text!"] }, "ROS": { diff --git a/src/SnippetSession.tsx b/src/SnippetSession.tsx index ebc9f90..bce3140 100644 --- a/src/SnippetSession.tsx +++ b/src/SnippetSession.tsx @@ -1,4 +1,5 @@ import { Editor, Range, RangeRef, Transforms } from 'slate'; +import { PlaceholderDecorationRange } from './customTypes'; import { isPointAtBlockStart } from './slateHelpers'; import { Placeholder, @@ -33,8 +34,10 @@ export class SnippetSession { this._placeholders.sort(Placeholder.compareByIndex); this._placeholderRanges = new Map(); this._placeholderIdx = -1; - Transforms.insertFragment(this._editor, this._snippet.toFragment(), { + const fragment = this._snippet.toFragment(); + Transforms.insertFragment(this._editor, fragment, { at: this._range.current!, + voids: true, }); this._placeholders.forEach(placeholder => { @@ -49,6 +52,7 @@ export class SnippetSession { : Editor.after(this._editor, range.anchor, { distance: placeholderStartOffset, unit: 'character', + voids: true, }); // this fixes a bug when the user inserts a snippet which starts with a placeholder @@ -65,6 +69,7 @@ export class SnippetSession { anchor = Editor.after(this._editor, anchor, { distance: 1, unit: 'offset', + voids: true, }); } @@ -74,6 +79,7 @@ export class SnippetSession { : Editor.after(this._editor, range.anchor, { distance: placeholderEndOffset, unit: 'character', + voids: true, }); if (anchor === undefined || focus === undefined) { @@ -102,14 +108,10 @@ export class SnippetSession { const nextPlaceholder = this._placeholders[this._placeholderIdx]; const nextRange = this._placeholderRanges!.get(nextPlaceholder)!.current!; - Transforms.select(this._editor, { - anchor: Editor.after(this._editor, nextRange.anchor, { - unit: 'character', - })!, - focus: Editor.before(this._editor, nextRange.focus, { - unit: 'character', - })!, - }); + Transforms.select( + this._editor, + this.transformPlaceholderRangeToSelectionRange(nextRange) + ); if (nextPlaceholder.isFinalTabstop) { this.dispose(); @@ -120,19 +122,42 @@ export class SnippetSession { return { done: false }; } + public transformPlaceholderRangeToSelectionRange( + nextRange: T + ): T { + return { + ...nextRange, + anchor: Editor.after(this._editor, nextRange.anchor, { + unit: 'character', + voids: true, + })!, + focus: Editor.before(this._editor, nextRange.focus, { + unit: 'character', + voids: true, + })!, + }; + } + public dispose() { for (let rangeRef of this._placeholderRanges?.values() ?? []) { rangeRef.unref(); } } - public get placeholderRanges() { + public get placeholderRanges(): PlaceholderDecorationRange[] { const ranges = this._placeholders .map(placeholder => { const rangeRef = this._placeholderRanges!.get(placeholder); - return rangeRef; + return { rangeRef, placeholder }; }) - .map(rangeRef => rangeRef!.current!); + .map(({ rangeRef, placeholder }) => { + const decorationRange: PlaceholderDecorationRange = { + ...rangeRef!.current!, + type: 'PlaceholderDecorationRange', + isFinalTabStop: placeholder.isFinalTabstop, + }; + return decorationRange; + }); return ranges; } } diff --git a/src/components/PlaceholderDecoration.tsx b/src/components/PlaceholderDecoration.tsx new file mode 100644 index 0000000..9af9400 --- /dev/null +++ b/src/components/PlaceholderDecoration.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { RenderLeafProps } from 'slate-react'; +import { PlaceholderDecorationText } from '../customTypes'; + +export const PlaceholderDecoration = ({ + attributes, + children, + leaf, + text, + placeholderColor, +}: Omit & { + leaf: PlaceholderDecorationText; + text: PlaceholderDecorationText; + placeholderColor: string; +}) => { + if (leaf.text.replaceAll('\u200B', '').length === 0) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); +}; diff --git a/src/components/SnippetReadonlyText.tsx b/src/components/SnippetReadonlyText.tsx new file mode 100644 index 0000000..2b83fd0 --- /dev/null +++ b/src/components/SnippetReadonlyText.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { RenderElementProps, useFocused, useSelected } from 'slate-react'; +import { SnippetReadonlyTextElement } from '../customTypes'; + +export const SnippetReadonlyText = ({ + attributes, + children, + element, +}: Omit & { + element: SnippetReadonlyTextElement; +}) => { + const selected = useSelected(); + const focused = useFocused(); + return ( + + {element.label} + {children} + + ); +}; diff --git a/src/customTypes.ts b/src/customTypes.ts index b82cef7..ca446af 100644 --- a/src/customTypes.ts +++ b/src/customTypes.ts @@ -1,7 +1,8 @@ -import { BaseRange, BaseText } from 'slate'; +import { BaseElement, BaseRange, BaseText } from 'slate'; export type PlaceholderDecorationRange = { type: 'PlaceholderDecorationRange'; + isFinalTabStop: boolean; } & BaseRange; export type DefaultRange = { @@ -10,15 +11,26 @@ export type DefaultRange = { export type PlaceholderDecorationText = { type: 'PlaceholderDecorationRange'; + isFinalTabStop: boolean; } & BaseText; export type DefaultText = { type?: undefined; } & BaseText; +export type SnippetReadonlyTextElement = { + type: 'SnippetReadonlyText'; + label: string; +} & BaseElement; + +export type DefaultElement = { + type?: undefined; +} & BaseElement; + declare module 'slate' { interface CustomTypes { Range: PlaceholderDecorationRange | DefaultRange; Text: PlaceholderDecorationText | DefaultText; + Element: SnippetReadonlyTextElement | DefaultElement; } } diff --git a/src/slateHelpers.tsx b/src/slateHelpers.tsx index 97fb611..d65583f 100644 --- a/src/slateHelpers.tsx +++ b/src/slateHelpers.tsx @@ -42,9 +42,9 @@ export const isSelectionCollapsed = (s: Selection): s is Selection => { return s !== null && Range.isCollapsed(s); }; -const getEditorText = (e: Editor, at?: Location | null) => { +export const getEditorText = (e: Editor, at?: Location | null) => { if (at !== null && at !== undefined) { - return Editor.string(e, at); + return Editor.string(e, at, { voids: true }); } return ''; }; @@ -53,6 +53,7 @@ export const isPointAtBlockStart = (e: Editor, point: Point) => { const [_, path] = Editor.above(e, { at: point, match: n => Editor.isBlock(e, n), + voids: true, }) ?? [undefined, undefined]; return path !== undefined && Editor.isStart(e, point, path); }; @@ -66,7 +67,7 @@ export const matchesTriggerAndPattern = ( { at, trigger, pattern }: { at: Point; trigger: string; pattern: string } ) => { // Point at the start of line - const lineStart = Editor.before(editor, at, { unit: 'line' }); + const lineStart = Editor.before(editor, at, { unit: 'line', voids: true }); // Range from before to start const beforeRange = lineStart && Editor.range(editor, lineStart, at); @@ -87,6 +88,7 @@ export const matchesTriggerAndPattern = ( ? Editor.before(editor, at, { unit: 'character', distance: match[1].length + trigger.length, + voids: true, }) : null; diff --git a/src/slateSnippetsExtension.tsx b/src/slateSnippetsExtension.tsx index f1badaf..0804616 100644 --- a/src/slateSnippetsExtension.tsx +++ b/src/slateSnippetsExtension.tsx @@ -2,7 +2,8 @@ import isHotkey from 'is-hotkey'; import React, { useCallback, useState } from 'react'; import { Editor, Node, Range, Text, Transforms } from 'slate'; import { SlateExtension } from 'use-slate-with-extensions'; -import { PlaceholderDecorationRange } from './customTypes'; +import { PlaceholderDecoration } from './components/PlaceholderDecoration'; +import { SnippetReadonlyText } from './components/SnippetReadonlyText'; import { isPointAtWordEnd, isRangeContained, @@ -43,45 +44,56 @@ export const useSlateSnippetsExtension = ( }, [snippetSession]); return { + isVoid: (element, editor, next) => { + if (element.type === 'SnippetReadonlyText') { + return true; + } + + return next(element, editor); + }, + isVoidDeps: [], + isInline: (element, editor, next) => { + if (element.type === 'SnippetReadonlyText') { + return true; + } + return next(element, editor); + }, + isInlineDeps: [], decorate: ([node, path], editor) => { if (snippetSession !== undefined && Text.isText(node)) { const [start, end] = Editor.edges(editor, path); const nodeRange = { anchor: start, focus: end }; const placeholderRanges = snippetSession.placeholderRanges + .map(placeholderRange => + snippetSession.transformPlaceholderRangeToSelectionRange( + placeholderRange + ) + ) .filter(placeholderRange => isRangeContained(nodeRange, placeholderRange) - ) - .map(r => { - const decorationRange: PlaceholderDecorationRange = { - ...r, - type: 'PlaceholderDecorationRange', - }; - return decorationRange; - }); + ); return placeholderRanges; } return undefined; }, decorateDeps: [snippetSession], + renderElement: props => { + const { element } = props; + if (element.type === 'SnippetReadonlyText') { + return ; + } + return undefined; + }, + renderElementDeps: [], renderLeaf: props => { if (props.leaf.type === 'PlaceholderDecorationRange') { - if (props.leaf.text.replaceAll('\u200B', '').length === 0) { - return ( - - {props.children} - - ); - } return ( - {props.children} + ); } return undefined; @@ -167,7 +179,7 @@ export const useSlateSnippetsExtension = ( Transforms.insertText( editor, Node.string(node).replaceAll('\u200B', ''), - { at: path } + { at: path, voids: true } ); return; } diff --git a/src/snippetParser/snippetParser.ts b/src/snippetParser/snippetParser.ts index 8c1cd68..feda900 100644 --- a/src/snippetParser/snippetParser.ts +++ b/src/snippetParser/snippetParser.ts @@ -247,7 +247,18 @@ export class Text extends Marker { const isNotLast = i < a.length - 1; const result: (Descendant | 'line-break')[] = []; if (v.length > 0) { - result.push({ text: v }); + if ( + this.parent instanceof Placeholder || + this.parent instanceof Choice + ) { + result.push({ text: v }); + } else { + result.push({ + children: [{ text: v }], + type: 'SnippetReadonlyText', + label: v, + }); + } } if (isNotLast) { result.push('line-break');