11import { tracked } from " @glimmer/tracking" ;
22import { later } from " @ember/runloop" ;
33import loadJSDiff from " discourse/lib/load-js-diff" ;
4- import { IMAGE_MARKDOWN_REGEX } from " discourse/lib/uploads " ;
4+ import { parseAsync } from " discourse/lib/text " ;
55
66const DEFAULT_CHAR_TYPING_DELAY = 30 ;
77
@@ -12,6 +12,7 @@ export default class DiffStreamer {
1212 @tracked diff = " " ;
1313 @tracked suggestion = " " ;
1414 @tracked isDone = false ;
15+ @tracked isThinking = false ;
1516
1617 typingTimer = null ;
1718 currentWordIndex = 0 ;
@@ -37,6 +38,7 @@ export default class DiffStreamer {
3738 this .isDone = !! result? .done ;
3839
3940 if (newText .length < this .lastResultText .length ) {
41+ this .isThinking = false ;
4042 // reset if text got shorter (e.g., reset or new input)
4143 this .words = [];
4244 this .suggestion = " " ;
@@ -45,11 +47,17 @@ export default class DiffStreamer {
4547 }
4648
4749 const diffText = newText .slice (this .lastResultText .length );
50+
4851 if (! diffText .trim ()) {
4952 this .lastResultText = newText;
5053 return ;
5154 }
5255
56+ if (await this .#isIncompleteMarkdown (diffText)) {
57+ this .isThinking = true ;
58+ return ;
59+ }
60+
5361 const newWords = this .#tokenizeMarkdownAware (diffText);
5462
5563 if (newWords .length > 0 ) {
@@ -63,32 +71,6 @@ export default class DiffStreamer {
6371 this .lastResultText = newText;
6472 }
6573
66- #tokenizeMarkdownAware (text ) {
67- const tokens = [];
68- let lastIndex = 0 ;
69-
70- let match;
71- while ((match = IMAGE_MARKDOWN_REGEX .exec (text)) !== null ) {
72- const matchStart = match .index ;
73-
74- if (lastIndex < matchStart) {
75- const preceding = text .slice (lastIndex, matchStart);
76- tokens .push (... (preceding .match (/ \S + \s * | \s + / g ) || []));
77- }
78-
79- tokens .push (match[0 ]);
80-
81- lastIndex = IMAGE_MARKDOWN_REGEX .lastIndex ;
82- }
83-
84- if (lastIndex < text .length ) {
85- const trailing = text .slice (lastIndex);
86- tokens .push (... (trailing .match (/ \S + \s * | \s + / g ) || []));
87- }
88-
89- return tokens;
90- }
91-
9274 reset () {
9375 this .diff = " " ;
9476 this .suggestion = " " ;
@@ -103,16 +85,34 @@ export default class DiffStreamer {
10385 }
10486 }
10587
88+ async #isIncompleteMarkdown (text ) {
89+ const tokens = await parseAsync (text);
90+
91+ const hasImage = tokens .some ((t ) => t .type === " image" );
92+ const hasLink = tokens .some ((t ) => t .type === " link_open" );
93+
94+ if (hasImage || hasLink) {
95+ return false ;
96+ }
97+
98+ const maybeUnfinishedImage =
99+ / !\[ [^ \] ] * $ / .test (text) || / !\[ [^ \] ] * ]\( upload:\/\/ [^ \s )] + $ / .test (text);
100+
101+ const maybeUnfinishedLink =
102+ / \[ [^ \] ] * $ / .test (text) || / \[ [^ \] ] * ]\( [^ \s )] + $ / .test (text);
103+
104+ return maybeUnfinishedImage || maybeUnfinishedLink;
105+ }
106+
106107 async #streamNextChar () {
107108 if (this .currentWordIndex < this .words .length ) {
108109 const currentToken = this .words [this .currentWordIndex ];
109110
110- const isMarkdownToken =
111- currentToken .startsWith (" ![" ) || currentToken .startsWith (" [" );
112-
113- if (isMarkdownToken) {
114- this .suggestion += currentToken;
111+ const nextChar = currentToken .charAt (this .currentCharIndex );
112+ this .suggestion += nextChar;
113+ this .currentCharIndex ++ ;
115114
115+ if (this .currentCharIndex >= currentToken .length ) {
116116 this .currentWordIndex ++ ;
117117 this .currentCharIndex = 0 ;
118118
@@ -126,31 +126,9 @@ export default class DiffStreamer {
126126 if (this .currentWordIndex === 1 ) {
127127 this .diff = this .diff .replace (/ ^ \s + / , " " );
128128 }
129-
130- this .typingTimer = later (this , this .#streamNextChar, this .typingDelay );
131- } else {
132- const nextChar = currentToken .charAt (this .currentCharIndex );
133- this .suggestion += nextChar;
134- this .currentCharIndex ++ ;
135-
136- if (this .currentCharIndex >= currentToken .length ) {
137- this .currentWordIndex ++ ;
138- this .currentCharIndex = 0 ;
139-
140- const originalDiff = this .jsDiff .diffWordsWithSpace (
141- this .selectedText ,
142- this .suggestion
143- );
144-
145- this .diff = this .#formatDiffWithTags (originalDiff);
146-
147- if (this .currentWordIndex === 1 ) {
148- this .diff = this .diff .replace (/ ^ \s + / , " " );
149- }
150- }
151-
152- this .typingTimer = later (this , this .#streamNextChar, this .typingDelay );
153129 }
130+
131+ this .typingTimer = later (this , this .#streamNextChar, this .typingDelay );
154132 } else {
155133 if (! this .suggestion || ! this .selectedText || ! this .jsDiff ) {
156134 return ;
@@ -167,6 +145,33 @@ export default class DiffStreamer {
167145 }
168146 }
169147
148+ #tokenizeMarkdownAware (text ) {
149+ const tokens = [];
150+ let lastIndex = 0 ;
151+ const regex = / !\[ [^ \] ] * ]\( upload:\/\/ [^ \s )] + \) / g ;
152+
153+ let match;
154+ while ((match = regex .exec (text)) !== null ) {
155+ const matchStart = match .index ;
156+
157+ if (lastIndex < matchStart) {
158+ const before = text .slice (lastIndex, matchStart);
159+ tokens .push (... (before .match (/ \S + \s * | \s + / g ) || []));
160+ }
161+
162+ tokens .push (match[0 ]);
163+
164+ lastIndex = regex .lastIndex ;
165+ }
166+
167+ if (lastIndex < text .length ) {
168+ const rest = text .slice (lastIndex);
169+ tokens .push (... (rest .match (/ \S + \s * | \s + / g ) || []));
170+ }
171+
172+ return tokens;
173+ }
174+
170175 #wrapChunk (text , type ) {
171176 if (type === " added" ) {
172177 return ` <ins>${ text} </ins>` ;
0 commit comments