@@ -16,11 +16,54 @@ limitations under the License.
1616
1717import Range from "./range" ;
1818import { Part , Type } from "./parts" ;
19+ import { Formatting } from "../components/views/rooms/MessageComposerFormatBar" ;
1920
2021/**
2122 * Some common queries and transformations on the editor model
2223 */
2324
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+
2467export function replaceRangeAndExpandSelection ( range : Range , newParts : Part [ ] ) : void {
2568 const { model } = range ;
2669 model . transform ( ( ) => {
@@ -32,17 +75,69 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]):
3275 } ) ;
3376}
3477
35- export function replaceRangeAndMoveCaret ( range : Range , newParts : Part [ ] , offset = 0 ) : void {
78+ export function replaceRangeAndMoveCaret ( range : Range , newParts : Part [ ] , offset = 0 , atNodeEnd = false ) : void {
3679 const { model } = range ;
3780 model . transform ( ( ) => {
3881 const oldLen = range . length ;
3982 const addedLen = range . replace ( newParts ) ;
4083 const firstOffset = range . start . asOffset ( model ) ;
41- const lastOffset = firstOffset . add ( oldLen + addedLen + offset ) ;
84+ const lastOffset = firstOffset . add ( oldLen + addedLen + offset , atNodeEnd ) ;
4285 return lastOffset . asPosition ( model ) ;
4386 } ) ;
4487}
4588
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+
46141export function rangeStartsAtBeginningOfLine ( range : Range ) : boolean {
47142 const { model } = range ;
48143 const startsWithPartial = range . start . offset !== 0 ;
@@ -76,16 +171,29 @@ export function formatRangeAsQuote(range: Range): void {
76171 if ( ! rangeEndsAtEndOfLine ( range ) ) {
77172 parts . push ( partCreator . newline ( ) ) ;
78173 }
79-
80174 parts . push ( partCreator . newline ( ) ) ;
81175 replaceRangeAndExpandSelection ( range , parts ) ;
82176}
83177
84178export function formatRangeAsCode ( range : Range ) : void {
85179 const { model, parts } = range ;
86180 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 ) {
89197 parts . unshift ( partCreator . plain ( "```" ) , partCreator . newline ( ) ) ;
90198 if ( ! rangeStartsAtBeginningOfLine ( range ) ) {
91199 parts . unshift ( partCreator . newline ( ) ) ;
@@ -97,19 +205,28 @@ export function formatRangeAsCode(range: Range): void {
97205 parts . push ( partCreator . newline ( ) ) ;
98206 }
99207 } else {
100- parts . unshift ( partCreator . plain ( "`" ) ) ;
101- parts . push ( partCreator . plain ( "`" ) ) ;
208+ toggleInlineFormat ( range , "`" ) ;
209+ return ;
102210 }
211+
103212 replaceRangeAndExpandSelection ( range , parts ) ;
104213}
105214
106215export function formatRangeAsLink ( range : Range ) {
107- const { model, parts } = range ;
216+ const { model } = range ;
108217 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+ }
113230}
114231
115232// parts helper methods
@@ -162,7 +279,7 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
162279 parts [ index - 1 ] . text . endsWith ( suffix ) ;
163280
164281 if ( isFormatted ) {
165- // remove prefix and suffix
282+ // remove prefix and suffix formatting string
166283 const partWithoutPrefix = parts [ base ] . serialize ( ) ;
167284 partWithoutPrefix . text = partWithoutPrefix . text . substr ( prefix . length ) ;
168285 parts [ base ] = partCreator . deserializePart ( partWithoutPrefix ) ;
@@ -178,5 +295,13 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
178295 }
179296 } ) ;
180297
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+ }
182307}
0 commit comments