Skip to content
Snippets Groups Projects
Commit 2932ce15 authored by Loïc Correnson's avatar Loïc Correnson
Browse files

[dome/richtext] range proxy & viewport listener

parent 87a4ac53
No related branches found
No related tags found
No related merge requests found
......@@ -115,6 +115,11 @@ function updateContents(view: CM.EditorView, newText: string): void {
Methods of the class are no-ops when there is no associated view, and at most
one component shall be associated with a given Text buffer at the same time.
<b>Warning:</n> do not access proxy's methods during React component
rendering since they would not be synchronized with further changes from
document or editor view. Rather, those methods shall be invoked from
React and event callbacks.
All methods are bound to `this`. */
export class TextProxy {
......@@ -123,6 +128,7 @@ export class TextProxy {
protected proxy : View = null;
constructor() {
this.range = this.range.bind(this);
this.clear = this.clear.bind(this);
this.append = this.append.bind(this);
this.toString = this.toString.bind(this);
......@@ -135,21 +141,33 @@ export class TextProxy {
// --- Public part
/** Full document range. Remark: empty documents still have 1 (empty) line. */
range(): Selection {
const view = this.proxy;
if (view === null) return emptySelection;
const doc = view.state.doc;
return { offset: 0, length: doc.length, fromLine: 1, toLine: doc.lines };
}
/** Remove all text from document. */
clear(): void {
const view = this.proxy;
if (view) dispatchContents(view, CS.Text.empty);
}
/** Full document contents. */
toString(): string {
const view = this.proxy;
return view ? view.state.doc.toString() : '';
}
/** Appends to end of document. */
append(data: string): void {
const view = this.proxy;
if (view) appendContents(view, data);
}
/** Appends to end of document. */
setContents(data: string): void {
const view = this.proxy;
if (view) dispatchContents(view, data);
......@@ -181,13 +199,19 @@ function textOf(text: string): CS.Text {
export class TextBuffer extends TextProxy {
// --- Private part (we avoid unecessary conversions from/to text)
// --- Invariant: only one of proxy, text & contents holds data
// --- Invariant: only one of proxy, text or contents holds data
private text = CS.Text.empty;
private contents : string | undefined = undefined;
private toText(): CS.Text {
// --- requires this.proxy is null
const contents = this.contents;
return contents === undefined ? this.text : textOf(contents);
if (contents===undefined) return this.text;
const text = textOf(contents);
this.text = text;
this.contents = undefined;
// --- invariant established
return text;
}
/** @ignore */
......@@ -210,6 +234,12 @@ export class TextBuffer extends TextProxy {
// --- Public part
range(): Selection {
if (this.proxy) return super.range();
const doc = this.toText();
return { offset: 0, length: doc.length, fromLine: 1, toLine: doc.lines };
}
clear(): void {
const view = this.proxy;
if (view) dispatchContents(view, CS.Text.empty);
......@@ -379,6 +409,30 @@ OnSelect.pack(
}
));
/* -------------------------------------------------------------------------- */
/* --- Viewport Change Listener --- */
/* -------------------------------------------------------------------------- */
const Viewport = new Field<SelectionCallback|null>(null);
Viewport.pack(
CM.EditorView.updateListener.computeN(
[Viewport.field],
(state) => {
const callback = state.field(Viewport.field);
if (callback !== null)
return [
(updates: CM.ViewUpdate) => {
if (updates.viewportChanged) {
const sel = updates.view.viewport;
const doc = updates.state.doc;
callback(selection(doc, sel));
}
}];
return [];
}
));
/* -------------------------------------------------------------------------- */
/* --- Decorations --- */
/* -------------------------------------------------------------------------- */
......@@ -661,6 +715,7 @@ function createView(parent: Element): CM.EditorView {
ReadOnly,
OnChange,
OnSelect,
Viewport,
Decorations,
];
const state = CS.EditorState.create({ extensions });
......@@ -676,6 +731,7 @@ export interface TextViewProps {
readOnly?: boolean;
onChange?: Callback;
selection?: Range;
onViewport?: SelectionCallback;
onSelection?: SelectionCallback;
decorations?: Decorations;
lineNumbers?: boolean;
......@@ -701,7 +757,9 @@ export function TextView(props: TextViewProps) : JSX.Element {
// ---- readOnly, onChange, onSelection, lineNumbers
const {
readOnly = false, onChange = null,
readOnly = false,
onChange = null,
onViewport: onReview = null,
onSelection: onSelect = null,
lineNumbers: lines,
showCurrentLine: active,
......@@ -709,6 +767,7 @@ export function TextView(props: TextViewProps) : JSX.Element {
React.useEffect(() => ReadOnly.dispatch(view, readOnly), [view, readOnly]);
React.useEffect(() => OnChange.dispatch(view, onChange), [view, onChange]);
React.useEffect(() => OnSelect.dispatch(view, onSelect), [view, onSelect]);
React.useEffect(() => Viewport.dispatch(view, onReview), [view, onReview]);
React.useEffect(() => LineNumbers.dispatch(view, lines), [view, lines]);
React.useEffect(() => ActiveLine.dispatch(view, active), [view, active]);
......
......@@ -44,28 +44,32 @@ import { registerSandbox } from 'ivette';
/* -------------------------------------------------------------------------- */
function UseText(): JSX.Element {
const [prefix, setPrefix] = React.useState('');
const [useLines, flipUseLines] = Dome.useFlipState(true);
const [useCurrent, flipUseCurrent] = Dome.useFlipState(true);
const [readOnly, flipReadOnly] = Dome.useFlipState(false);
const [useProxy, flipUseProxy] = Dome.useFlipState(false);
const [changed, setChanged] = React.useState(false);
const [changes, setChanges] = React.useState(0);
const [length, setLength] = React.useState(0);
const [lines, setLines] = React.useState(1);
const [s, onSelection] = React.useState(emptySelection);
const [v, onViewport] = React.useState(emptySelection);
const proxy = React.useMemo(() => new TextProxy(), []);
const buffer = React.useMemo(() => new TextBuffer(), []);
const text = useProxy ? proxy : buffer;
const updatePrefix = React.useCallback(
const updateProxy = React.useCallback(
() => {
const { length, toLine } = text.range();
setChanged(true);
setChanges((n) => 1+n);
setPrefix(text.toString().substring(0, 20).trim());
setLength(length);
setLines(toLine);
}, [text]);
const push = React.useCallback(() => {
const n = Math.random();
text.append(`ADDED${n}\n`);
}, [text]);
const onChange = Dome.useDebounced(updatePrefix, 200);
const onChange = Dome.useDebounced(updateProxy, 200);
const [decorations, setDecorations] = React.useState<Decoration[]>([]);
const inconsistent = decorations.length > 0 && changed;
......@@ -107,8 +111,8 @@ function UseText(): JSX.Element {
}]);
}, [decorations, s]);
const isLine = s.fromLine === s.toLine;
const isRange = s.length > 0;
const isLine = s.fromLine === s.toLine && s.toLine <= lines;
const isRange = s.length > 0 && s.offset + s.length <= length;
return (
<>
......@@ -135,39 +139,36 @@ function UseText(): JSX.Element {
title={useProxy ? 'Use TextProxy' : 'Use TextBuffer (persistent)'}
onClick={flipUseProxy}
/>
<Code label={`Offset ${s.offset}-${s.offset + s.length}`} />
<Code label={`Line ${s.fromLine}-${s.toLine}`} />
<Filler/>
<Code
icon={inconsistent ? 'WARNING' : undefined}
title={inconsistent ? 'Iconsistent (modified text)' : undefined}
label={`Decorations ${decorations.length}`}
label={`Decorations: ${decorations.length}`}
/>
<IconButton
display={isLine}
enabled={isLine}
icon="CIRC.INFO"
title="Add Gutter Decoration"
onClick={addGutterDecoration}
/>
<IconButton
display={isLine}
enabled={isLine}
icon="CIRC.CHECK"
title="Add Line Decoration"
onClick={addLineDecoration}
/>
<IconButton
display={isRange}
enabled={isRange}
icon="CIRC.PLUS"
title="Add Decoration"
onClick={addDecoration}
/>
<IconButton
display={decorations.length > 0}
enabled={decorations.length > 0}
kind={inconsistent ? 'negative' : 'default'}
icon="CIRC.CLOSE"
title="Clear Decorations"
onClick={clearDecorations} />
<Filler />
<Code>{`"${prefix}" (${changes})`}</Code>
<Button label="Push" onClick={push} />
<Button label="Clear" kind='negative' onClick={clearText} />
</ToolBar>
......@@ -176,10 +177,18 @@ function UseText(): JSX.Element {
readOnly={readOnly}
onChange={onChange}
onSelection={onSelection}
onViewport={onViewport}
decorations={decorations}
lineNumbers={useLines}
showCurrentLine={useCurrent}
/>
<ToolBar>
<Code label={`Offset ${s.offset}-${s.offset + s.length} / ${length}`} />
<Code label={`Line ${s.fromLine}-${s.toLine} / ${lines}`} />
<Code label={`View ${v.fromLine}-${v.toLine}`} />
<Filler />
<Code>{`Changes: ${changes}`}</Code>
</ToolBar>
</>
);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment