@uiw/react-codemirror 4.23.13 → 4.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ export declare class TimeoutLatch {
2
+ private timeLeftMS;
3
+ private timeoutMS;
4
+ private isCancelled;
5
+ private isTimeExhausted;
6
+ private callbacks;
7
+ constructor(callback: Function, timeoutMS: number);
8
+ tick(): void;
9
+ cancel(): void;
10
+ reset(): void;
11
+ get isDone(): boolean;
12
+ }
13
+ declare class Scheduler {
14
+ private interval;
15
+ private latches;
16
+ add(latch: TimeoutLatch): void;
17
+ remove(latch: TimeoutLatch): void;
18
+ private start;
19
+ private stop;
20
+ }
21
+ export declare const getScheduler: () => Scheduler;
22
+ export {};
@@ -0,0 +1,88 @@
1
+ // Setting / Unsetting timeouts for every keystroke was a significant overhead
2
+ // Inspired from https://github.com/iostreamer-X/timeout-latch
3
+
4
+ export class TimeoutLatch {
5
+ constructor(callback, timeoutMS) {
6
+ this.timeLeftMS = void 0;
7
+ this.timeoutMS = void 0;
8
+ this.isCancelled = false;
9
+ this.isTimeExhausted = false;
10
+ this.callbacks = [];
11
+ this.timeLeftMS = timeoutMS;
12
+ this.timeoutMS = timeoutMS;
13
+ this.callbacks.push(callback);
14
+ }
15
+ tick() {
16
+ if (!this.isCancelled && !this.isTimeExhausted) {
17
+ this.timeLeftMS--;
18
+ if (this.timeLeftMS <= 0) {
19
+ this.isTimeExhausted = true;
20
+ var callbacks = this.callbacks.slice();
21
+ this.callbacks.length = 0;
22
+ callbacks.forEach(callback => {
23
+ try {
24
+ callback();
25
+ } catch (error) {
26
+ console.error('TimeoutLatch callback error:', error);
27
+ }
28
+ });
29
+ }
30
+ }
31
+ }
32
+ cancel() {
33
+ this.isCancelled = true;
34
+ this.callbacks.length = 0;
35
+ }
36
+ reset() {
37
+ this.timeLeftMS = this.timeoutMS;
38
+ this.isCancelled = false;
39
+ this.isTimeExhausted = false;
40
+ }
41
+ get isDone() {
42
+ return this.isCancelled || this.isTimeExhausted;
43
+ }
44
+ }
45
+ class Scheduler {
46
+ constructor() {
47
+ this.interval = null;
48
+ this.latches = new Set();
49
+ }
50
+ add(latch) {
51
+ this.latches.add(latch);
52
+ this.start();
53
+ }
54
+ remove(latch) {
55
+ this.latches.delete(latch);
56
+ if (this.latches.size === 0) {
57
+ this.stop();
58
+ }
59
+ }
60
+ start() {
61
+ if (this.interval === null) {
62
+ this.interval = setInterval(() => {
63
+ this.latches.forEach(latch => {
64
+ latch.tick();
65
+ if (latch.isDone) {
66
+ this.remove(latch);
67
+ }
68
+ });
69
+ }, 1);
70
+ }
71
+ }
72
+ stop() {
73
+ if (this.interval !== null) {
74
+ clearInterval(this.interval);
75
+ this.interval = null;
76
+ }
77
+ }
78
+ }
79
+ var globalScheduler = null;
80
+ export var getScheduler = () => {
81
+ if (typeof window === 'undefined') {
82
+ return new Scheduler();
83
+ }
84
+ if (!globalScheduler) {
85
+ globalScheduler = new Scheduler();
86
+ }
87
+ return globalScheduler;
88
+ };
@@ -1,6 +1,7 @@
1
1
  import { EditorState } from '@codemirror/state';
2
2
  import { EditorView } from '@codemirror/view';
3
3
  import { type ReactCodeMirrorProps } from '.';
4
+ export declare const ExternalChange: import("@codemirror/state").AnnotationType<boolean>;
4
5
  export interface UseCodeMirror extends ReactCodeMirrorProps {
5
6
  container?: HTMLDivElement | null;
6
7
  }
@@ -3,7 +3,10 @@ import { Annotation, EditorState, StateEffect } from '@codemirror/state';
3
3
  import { EditorView } from '@codemirror/view';
4
4
  import { getDefaultExtensions } from "./getDefaultExtensions.js";
5
5
  import { getStatistics } from "./utils.js";
6
- var External = Annotation.define();
6
+ import { TimeoutLatch, getScheduler } from "./timeoutLatch.js";
7
+ export var ExternalChange = Annotation.define();
8
+ var TYPING_TIMOUT = 200; // ms
9
+
7
10
  var emptyExtensions = [];
8
11
  export function useCodeMirror(props) {
9
12
  var {
@@ -33,6 +36,12 @@ export function useCodeMirror(props) {
33
36
  var [container, setContainer] = useState();
34
37
  var [view, setView] = useState();
35
38
  var [state, setState] = useState();
39
+ var typingLatch = useState(() => ({
40
+ current: null
41
+ }))[0];
42
+ var pendingUpdate = useState(() => ({
43
+ current: null
44
+ }))[0];
36
45
  var defaultThemeOption = EditorView.theme({
37
46
  '&': {
38
47
  height,
@@ -50,7 +59,20 @@ export function useCodeMirror(props) {
50
59
  if (vu.docChanged && typeof onChange === 'function' &&
51
60
  // Fix echoing of the remote changes:
52
61
  // If transaction is market as remote we don't have to call `onChange` handler again
53
- !vu.transactions.some(tr => tr.annotation(External))) {
62
+ !vu.transactions.some(tr => tr.annotation(ExternalChange))) {
63
+ if (typingLatch.current) {
64
+ typingLatch.current.reset();
65
+ } else {
66
+ typingLatch.current = new TimeoutLatch(() => {
67
+ if (pendingUpdate.current) {
68
+ var forceUpdate = pendingUpdate.current;
69
+ pendingUpdate.current = null;
70
+ forceUpdate();
71
+ }
72
+ typingLatch.current = null;
73
+ }, TYPING_TIMOUT);
74
+ getScheduler().add(typingLatch.current);
75
+ }
54
76
  var doc = vu.state.doc;
55
77
  var _value = doc.toString();
56
78
  onChange(_value, vu);
@@ -106,6 +128,10 @@ export function useCodeMirror(props) {
106
128
  view.destroy();
107
129
  setView(undefined);
108
130
  }
131
+ if (typingLatch.current) {
132
+ typingLatch.current.cancel();
133
+ typingLatch.current = null;
134
+ }
109
135
  }, [view]);
110
136
  useEffect(() => {
111
137
  if (autoFocus && view) {
@@ -126,14 +152,24 @@ export function useCodeMirror(props) {
126
152
  }
127
153
  var currentValue = view ? view.state.doc.toString() : '';
128
154
  if (view && value !== currentValue) {
129
- view.dispatch({
130
- changes: {
131
- from: 0,
132
- to: currentValue.length,
133
- insert: value || ''
134
- },
135
- annotations: [External.of(true)]
136
- });
155
+ var isTyping = typingLatch.current && !typingLatch.current.isDone;
156
+ var forceUpdate = () => {
157
+ if (view && value !== view.state.doc.toString()) {
158
+ view.dispatch({
159
+ changes: {
160
+ from: 0,
161
+ to: view.state.doc.toString().length,
162
+ insert: value || ''
163
+ },
164
+ annotations: [ExternalChange.of(true)]
165
+ });
166
+ }
167
+ };
168
+ if (!isTyping) {
169
+ forceUpdate();
170
+ } else {
171
+ pendingUpdate.current = forceUpdate;
172
+ }
137
173
  }
138
174
  }, [value, view]);
139
175
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uiw/react-codemirror",
3
- "version": "4.23.13",
3
+ "version": "4.24.0",
4
4
  "description": "CodeMirror component for React.",
5
5
  "homepage": "https://uiwjs.github.io/react-codemirror",
6
6
  "funding": "https://jaywcjlove.github.io/#/sponsor",
@@ -47,7 +47,7 @@
47
47
  "@codemirror/commands": "^6.1.0",
48
48
  "@codemirror/state": "^6.1.1",
49
49
  "@codemirror/theme-one-dark": "^6.0.0",
50
- "@uiw/codemirror-extensions-basic-setup": "4.23.13",
50
+ "@uiw/codemirror-extensions-basic-setup": "4.24.0",
51
51
  "codemirror": "^6.0.0"
52
52
  },
53
53
  "keywords": [
@@ -0,0 +1,98 @@
1
+ // Setting / Unsetting timeouts for every keystroke was a significant overhead
2
+ // Inspired from https://github.com/iostreamer-X/timeout-latch
3
+
4
+ export class TimeoutLatch {
5
+ private timeLeftMS: number;
6
+ private timeoutMS: number;
7
+ private isCancelled = false;
8
+ private isTimeExhausted = false;
9
+ private callbacks: Function[] = [];
10
+
11
+ constructor(callback: Function, timeoutMS: number) {
12
+ this.timeLeftMS = timeoutMS;
13
+ this.timeoutMS = timeoutMS;
14
+ this.callbacks.push(callback);
15
+ }
16
+
17
+ tick(): void {
18
+ if (!this.isCancelled && !this.isTimeExhausted) {
19
+ this.timeLeftMS--;
20
+ if (this.timeLeftMS <= 0) {
21
+ this.isTimeExhausted = true;
22
+ const callbacks = this.callbacks.slice();
23
+ this.callbacks.length = 0;
24
+ callbacks.forEach((callback) => {
25
+ try {
26
+ callback();
27
+ } catch (error) {
28
+ console.error('TimeoutLatch callback error:', error);
29
+ }
30
+ });
31
+ }
32
+ }
33
+ }
34
+
35
+ cancel(): void {
36
+ this.isCancelled = true;
37
+ this.callbacks.length = 0;
38
+ }
39
+
40
+ reset(): void {
41
+ this.timeLeftMS = this.timeoutMS;
42
+ this.isCancelled = false;
43
+ this.isTimeExhausted = false;
44
+ }
45
+
46
+ get isDone(): boolean {
47
+ return this.isCancelled || this.isTimeExhausted;
48
+ }
49
+ }
50
+
51
+ class Scheduler {
52
+ private interval: NodeJS.Timeout | null = null;
53
+ private latches = new Set<TimeoutLatch>();
54
+
55
+ add(latch: TimeoutLatch): void {
56
+ this.latches.add(latch);
57
+ this.start();
58
+ }
59
+
60
+ remove(latch: TimeoutLatch): void {
61
+ this.latches.delete(latch);
62
+ if (this.latches.size === 0) {
63
+ this.stop();
64
+ }
65
+ }
66
+
67
+ private start(): void {
68
+ if (this.interval === null) {
69
+ this.interval = setInterval(() => {
70
+ this.latches.forEach((latch) => {
71
+ latch.tick();
72
+ if (latch.isDone) {
73
+ this.remove(latch);
74
+ }
75
+ });
76
+ }, 1);
77
+ }
78
+ }
79
+
80
+ private stop(): void {
81
+ if (this.interval !== null) {
82
+ clearInterval(this.interval);
83
+ this.interval = null;
84
+ }
85
+ }
86
+ }
87
+
88
+ let globalScheduler: Scheduler | null = null;
89
+
90
+ export const getScheduler = (): Scheduler => {
91
+ if (typeof window === 'undefined') {
92
+ return new Scheduler();
93
+ }
94
+ if (!globalScheduler) {
95
+ globalScheduler = new Scheduler();
96
+ }
97
+ return globalScheduler;
98
+ };
@@ -4,8 +4,10 @@ import { EditorView, type ViewUpdate } from '@codemirror/view';
4
4
  import { getDefaultExtensions } from './getDefaultExtensions';
5
5
  import { getStatistics } from './utils';
6
6
  import { type ReactCodeMirrorProps } from '.';
7
+ import { TimeoutLatch, getScheduler } from './timeoutLatch';
7
8
 
8
- const External = Annotation.define<boolean>();
9
+ export const ExternalChange = Annotation.define<boolean>();
10
+ const TYPING_TIMOUT = 200; // ms
9
11
 
10
12
  export interface UseCodeMirror extends ReactCodeMirrorProps {
11
13
  container?: HTMLDivElement | null;
@@ -41,6 +43,8 @@ export function useCodeMirror(props: UseCodeMirror) {
41
43
  const [container, setContainer] = useState<HTMLDivElement | null>();
42
44
  const [view, setView] = useState<EditorView>();
43
45
  const [state, setState] = useState<EditorState>();
46
+ const typingLatch = useState<{ current: TimeoutLatch | null }>(() => ({ current: null }))[0];
47
+ const pendingUpdate = useState<{ current: (() => void) | null }>(() => ({ current: null }))[0];
44
48
  const defaultThemeOption = EditorView.theme({
45
49
  '&': {
46
50
  height,
@@ -60,8 +64,22 @@ export function useCodeMirror(props: UseCodeMirror) {
60
64
  typeof onChange === 'function' &&
61
65
  // Fix echoing of the remote changes:
62
66
  // If transaction is market as remote we don't have to call `onChange` handler again
63
- !vu.transactions.some((tr) => tr.annotation(External))
67
+ !vu.transactions.some((tr) => tr.annotation(ExternalChange))
64
68
  ) {
69
+ if (typingLatch.current) {
70
+ typingLatch.current.reset();
71
+ } else {
72
+ typingLatch.current = new TimeoutLatch(() => {
73
+ if (pendingUpdate.current) {
74
+ const forceUpdate = pendingUpdate.current;
75
+ pendingUpdate.current = null;
76
+ forceUpdate();
77
+ }
78
+ typingLatch.current = null;
79
+ }, TYPING_TIMOUT);
80
+ getScheduler().add(typingLatch.current);
81
+ }
82
+
65
83
  const doc = vu.state.doc;
66
84
  const value = doc.toString();
67
85
  onChange(value, vu);
@@ -126,6 +144,10 @@ export function useCodeMirror(props: UseCodeMirror) {
126
144
  view.destroy();
127
145
  setView(undefined);
128
146
  }
147
+ if (typingLatch.current) {
148
+ typingLatch.current.cancel();
149
+ typingLatch.current = null;
150
+ }
129
151
  },
130
152
  [view],
131
153
  );
@@ -165,10 +187,22 @@ export function useCodeMirror(props: UseCodeMirror) {
165
187
  }
166
188
  const currentValue = view ? view.state.doc.toString() : '';
167
189
  if (view && value !== currentValue) {
168
- view.dispatch({
169
- changes: { from: 0, to: currentValue.length, insert: value || '' },
170
- annotations: [External.of(true)],
171
- });
190
+ const isTyping = typingLatch.current && !typingLatch.current.isDone;
191
+
192
+ const forceUpdate = () => {
193
+ if (view && value !== view.state.doc.toString()) {
194
+ view.dispatch({
195
+ changes: { from: 0, to: view.state.doc.toString().length, insert: value || '' },
196
+ annotations: [ExternalChange.of(true)],
197
+ });
198
+ }
199
+ };
200
+
201
+ if (!isTyping) {
202
+ forceUpdate();
203
+ } else {
204
+ pendingUpdate.current = forceUpdate;
205
+ }
172
206
  }
173
207
  }, [value, view]);
174
208