@@ -16,11 +16,54 @@ limitations under the License.
16
16
17
17
import Range from "./range" ;
18
18
import { Part , Type } from "./parts" ;
19
+ import { Formatting } from "../components/views/rooms/MessageComposerFormatBar" ;
19
20
20
21
/**
21
22
* Some common queries and transformations on the editor model
22
23
*/
23
24
25
+ /**
26
+ * Formats a given range with a given action
27
+ * @param {Range } range the range that should be formatted
28
+ * @param {Formatting } action the action that should be performed on the range
29
+ */
30
+ export function formatRange ( range : Range , action : Formatting ) : void {
31
+ // If the selection was empty we select the current word instead
32
+ if ( range . wasInitializedEmpty ( ) ) {
33
+ selectRangeOfWordAtCaret ( range ) ;
34
+ } else {
35
+ // Remove whitespace or new lines in our selection
36
+ range . trim ( ) ;
37
+ }
38
+
39
+ // Edgecase when just selecting whitespace or new line.
40
+ // There should be no reason to format whitespace, so we can just return.
41
+ if ( range . length === 0 ) {
42
+ return ;
43
+ }
44
+
45
+ switch ( action ) {
46
+ case Formatting . Bold :
47
+ toggleInlineFormat ( range , "**" ) ;
48
+ break ;
49
+ case Formatting . Italics :
50
+ toggleInlineFormat ( range , "_" ) ;
51
+ break ;
52
+ case Formatting . Strikethrough :
53
+ toggleInlineFormat ( range , "<del>" , "</del>" ) ;
54
+ break ;
55
+ case Formatting . Code :
56
+ formatRangeAsCode ( range ) ;
57
+ break ;
58
+ case Formatting . Quote :
59
+ formatRangeAsQuote ( range ) ;
60
+ break ;
61
+ case Formatting . InsertLink :
62
+ formatRangeAsLink ( range ) ;
63
+ break ;
64
+ }
65
+ }
66
+
24
67
export function replaceRangeAndExpandSelection ( range : Range , newParts : Part [ ] ) : void {
25
68
const { model } = range ;
26
69
model . transform ( ( ) => {
@@ -32,17 +75,69 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]):
32
75
} ) ;
33
76
}
34
77
35
- export function replaceRangeAndMoveCaret ( range : Range , newParts : Part [ ] , offset = 0 ) : void {
78
+ export function replaceRangeAndMoveCaret ( range : Range , newParts : Part [ ] , offset = 0 , atNodeEnd = false ) : void {
36
79
const { model } = range ;
37
80
model . transform ( ( ) => {
38
81
const oldLen = range . length ;
39
82
const addedLen = range . replace ( newParts ) ;
40
83
const firstOffset = range . start . asOffset ( model ) ;
41
- const lastOffset = firstOffset . add ( oldLen + addedLen + offset ) ;
84
+ const lastOffset = firstOffset . add ( oldLen + addedLen + offset , atNodeEnd ) ;
42
85
return lastOffset . asPosition ( model ) ;
43
86
} ) ;
44
87
}
45
88
89
+ /**
90
+ * Replaces a range with formatting or removes existing formatting and
91
+ * positions the cursor with respect to the prefix and suffix length.
92
+ * @param {Range } range the previous value
93
+ * @param {Part[] } newParts the new value
94
+ * @param {boolean } rangeHasFormatting the new value
95
+ * @param {number } prefixLength the length of the formatting prefix
96
+ * @param {number } suffixLength the length of the formatting suffix, defaults to prefix length
97
+ */
98
+ export function replaceRangeAndAutoAdjustCaret (
99
+ range : Range ,
100
+ newParts : Part [ ] ,
101
+ rangeHasFormatting = false ,
102
+ prefixLength : number ,
103
+ suffixLength = prefixLength ,
104
+ ) : void {
105
+ const { model } = range ;
106
+ const lastStartingPosition = range . getLastStartingPosition ( ) ;
107
+ const relativeOffset = lastStartingPosition . offset - range . start . offset ;
108
+ const distanceFromEnd = range . length - relativeOffset ;
109
+ // Handle edge case where the caret is located within the suffix or prefix
110
+ if ( rangeHasFormatting ) {
111
+ if ( relativeOffset < prefixLength ) { // Was the caret at the left format string?
112
+ replaceRangeAndMoveCaret ( range , newParts , - ( range . length - 2 * suffixLength ) ) ;
113
+ return ;
114
+ }
115
+ if ( distanceFromEnd < suffixLength ) { // Was the caret at the right format string?
116
+ replaceRangeAndMoveCaret ( range , newParts , 0 , true ) ;
117
+ return ;
118
+ }
119
+ }
120
+ // Calculate new position with respect to the previous position
121
+ model . transform ( ( ) => {
122
+ const offsetDirection = Math . sign ( range . replace ( newParts ) ) ; // Compensates for shrinkage or expansion
123
+ const atEnd = distanceFromEnd === suffixLength ;
124
+ return lastStartingPosition . asOffset ( model ) . add ( offsetDirection * prefixLength , atEnd ) . asPosition ( model ) ;
125
+ } ) ;
126
+ }
127
+
128
+ const isFormattable = ( _index : number , offset : number , part : Part ) => {
129
+ return part . text [ offset ] !== " " && part . type === Type . Plain ;
130
+ } ;
131
+
132
+ export function selectRangeOfWordAtCaret ( range : Range ) : void {
133
+ // Select right side of word
134
+ range . expandForwardsWhile ( isFormattable ) ;
135
+ // Select left side of word
136
+ range . expandBackwardsWhile ( isFormattable ) ;
137
+ // Trim possibly selected new lines
138
+ range . trim ( ) ;
139
+ }
140
+
46
141
export function rangeStartsAtBeginningOfLine ( range : Range ) : boolean {
47
142
const { model } = range ;
48
143
const startsWithPartial = range . start . offset !== 0 ;
@@ -76,16 +171,29 @@ export function formatRangeAsQuote(range: Range): void {
76
171
if ( ! rangeEndsAtEndOfLine ( range ) ) {
77
172
parts . push ( partCreator . newline ( ) ) ;
78
173
}
79
-
80
174
parts . push ( partCreator . newline ( ) ) ;
81
175
replaceRangeAndExpandSelection ( range , parts ) ;
82
176
}
83
177
84
178
export function formatRangeAsCode ( range : Range ) : void {
85
179
const { model, parts } = range ;
86
180
const { partCreator } = model ;
87
- const needsBlock = parts . some ( p => p . type === Type . Newline ) ;
88
- if ( needsBlock ) {
181
+
182
+ const hasBlockFormatting = ( range . length > 0 )
183
+ && range . text . startsWith ( "```" )
184
+ && range . text . endsWith ( "```" ) ;
185
+
186
+ const needsBlockFormatting = parts . some ( p => p . type === Type . Newline ) ;
187
+
188
+ if ( hasBlockFormatting ) {
189
+ // Remove previously pushed backticks and new lines
190
+ parts . shift ( ) ;
191
+ parts . pop ( ) ;
192
+ if ( parts [ 0 ] ?. text === "\n" && parts [ parts . length - 1 ] ?. text === "\n" ) {
193
+ parts . shift ( ) ;
194
+ parts . pop ( ) ;
195
+ }
196
+ } else if ( needsBlockFormatting ) {
89
197
parts . unshift ( partCreator . plain ( "```" ) , partCreator . newline ( ) ) ;
90
198
if ( ! rangeStartsAtBeginningOfLine ( range ) ) {
91
199
parts . unshift ( partCreator . newline ( ) ) ;
@@ -97,19 +205,28 @@ export function formatRangeAsCode(range: Range): void {
97
205
parts . push ( partCreator . newline ( ) ) ;
98
206
}
99
207
} else {
100
- parts . unshift ( partCreator . plain ( "`" ) ) ;
101
- parts . push ( partCreator . plain ( "`" ) ) ;
208
+ toggleInlineFormat ( range , "`" ) ;
209
+ return ;
102
210
}
211
+
103
212
replaceRangeAndExpandSelection ( range , parts ) ;
104
213
}
105
214
106
215
export function formatRangeAsLink ( range : Range ) {
107
- const { model, parts } = range ;
216
+ const { model } = range ;
108
217
const { partCreator } = model ;
109
- parts . unshift ( partCreator . plain ( "[" ) ) ;
110
- parts . push ( partCreator . plain ( "]()" ) ) ;
111
- // We set offset to -1 here so that the caret lands between the brackets
112
- replaceRangeAndMoveCaret ( range , parts , - 1 ) ;
218
+ const linkRegex = / \[ ( .* ?) \] \( .* ?\) / g;
219
+ const isFormattedAsLink = linkRegex . test ( range . text ) ;
220
+ if ( isFormattedAsLink ) {
221
+ const linkDescription = range . text . replace ( linkRegex , "$1" ) ;
222
+ const newParts = [ partCreator . plain ( linkDescription ) ] ;
223
+ const prefixLength = 1 ;
224
+ const suffixLength = range . length - ( linkDescription . length + 2 ) ;
225
+ replaceRangeAndAutoAdjustCaret ( range , newParts , true , prefixLength , suffixLength ) ;
226
+ } else {
227
+ // We set offset to -1 here so that the caret lands between the brackets
228
+ replaceRangeAndMoveCaret ( range , [ partCreator . plain ( "[" + range . text + "]" + "()" ) ] , - 1 ) ;
229
+ }
113
230
}
114
231
115
232
// parts helper methods
@@ -162,7 +279,7 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
162
279
parts [ index - 1 ] . text . endsWith ( suffix ) ;
163
280
164
281
if ( isFormatted ) {
165
- // remove prefix and suffix
282
+ // remove prefix and suffix formatting string
166
283
const partWithoutPrefix = parts [ base ] . serialize ( ) ;
167
284
partWithoutPrefix . text = partWithoutPrefix . text . substr ( prefix . length ) ;
168
285
parts [ base ] = partCreator . deserializePart ( partWithoutPrefix ) ;
@@ -178,5 +295,13 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
178
295
}
179
296
} ) ;
180
297
181
- replaceRangeAndExpandSelection ( range , parts ) ;
298
+ // If the user didn't select something initially, we want to just restore
299
+ // the caret position instead of making a new selection.
300
+ if ( range . wasInitializedEmpty ( ) && prefix === suffix ) {
301
+ // Check if we need to add a offset for a toggle or untoggle
302
+ const hasFormatting = range . text . startsWith ( prefix ) && range . text . endsWith ( suffix ) ;
303
+ replaceRangeAndAutoAdjustCaret ( range , parts , hasFormatting , prefix . length ) ;
304
+ } else {
305
+ replaceRangeAndExpandSelection ( range , parts ) ;
306
+ }
182
307
}
0 commit comments