Skip to content

Commit 5350c45

Browse files
committed
Add function to convert plain strings to template strings.
1 parent d850177 commit 5350c45

File tree

2 files changed

+288
-14
lines changed

2 files changed

+288
-14
lines changed

typescript-mode-tests.el

+180-11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
(require 'ert)
1010
(require 'typescript-mode)
11+
(require 'cl)
1112

1213
(defun typescript-test-get-doc ()
1314
(buffer-substring-no-properties (point-min) (point-max)))
@@ -211,8 +212,18 @@ a severity set to WARNING, no rule name."
211212
`(with-temp-buffer
212213
(insert ,content)
213214
(typescript-mode)
214-
(font-lock-fontify-buffer)
215215
(goto-char (point-min))
216+
;; We need this so that tests that simulate user actions operate on the right buffer.
217+
(switch-to-buffer (current-buffer))
218+
,@body))
219+
220+
(defmacro test-with-fontified-buffer (content &rest body)
221+
"Fill a temporary buffer with `CONTENT' and eval `BODY' in it."
222+
(declare (debug t)
223+
(indent 1))
224+
`(test-with-temp-buffer
225+
,content
226+
(font-lock-fontify-buffer)
216227
,@body))
217228

218229
(defun get-face-at (loc)
@@ -234,7 +245,7 @@ put in the temporary buffer. `EXPECTED' is the expected
234245
results. It should be a list of (LOCATION . FACE) pairs, where
235246
LOCATION can be either a single location, or list of locations,
236247
that are all expected to have the same face."
237-
(test-with-temp-buffer
248+
(test-with-fontified-buffer
238249
contents
239250
;; Make sure our propertize function has been applied to the whole
240251
;; buffer.
@@ -263,7 +274,7 @@ documentation."
263274
(ert-deftest font-lock/no-documentation-in-non-documentation-comments ()
264275
"Documentation tags that are not in documentation comments
265276
should not be fontified as documentation."
266-
(test-with-temp-buffer
277+
(test-with-fontified-buffer
267278
(concat "/*\n" font-lock-contents "\n*/\n")
268279
(let ((loc 3))
269280
;; Make sure we start with the right face.
@@ -274,7 +285,7 @@ should not be fontified as documentation."
274285
(ert-deftest font-lock/no-documentation-in-strings ()
275286
"Documentation tags that are not in strings should not be
276287
fontified as documentation."
277-
(test-with-temp-buffer
288+
(test-with-fontified-buffer
278289
(concat "const x = \"/**" font-lock-contents "*/\";")
279290
(let ((loc (search-forward "\"")))
280291
;; Make sure we start with the right face.
@@ -374,16 +385,16 @@ declare function declareFunctionDefn(x3: xty3, y3: yty3): ret3;"
374385

375386
(ert-deftest font-lock/text-after-trailing-regexp-delim-should-not-be-fontified ()
376387
"Text after trailing regular expression delimiter should not be fontified."
377-
(test-with-temp-buffer
388+
(test-with-fontified-buffer
378389
"=/foo/g something // comment"
379390
(should (eq (get-face-at "g something") nil)))
380-
(test-with-temp-buffer
391+
(test-with-fontified-buffer
381392
"=/foo\\bar/g something // comment"
382393
(should (eq (get-face-at "g something") nil)))
383-
(test-with-temp-buffer
394+
(test-with-fontified-buffer
384395
"=/foo\\\\bar/g something // comment"
385396
(should (eq (get-face-at "g something") nil)))
386-
(test-with-temp-buffer
397+
(test-with-fontified-buffer
387398
"=/foo\\\\/g something // comment"
388399
(should (eq (get-face-at "g something") nil))))
389400

@@ -412,23 +423,181 @@ import... from...."
412423
;; to avoid hitting keywords. Moreover, the end position of the search is important.
413424
;; Flyspell puts point at the end of the word before calling the predicate. We must
414425
;; replicate that behavior here.
415-
(test-with-temp-buffer
426+
(test-with-fontified-buffer
416427
"import 'a';\nimport { x } from 'b';\nconst foo = 'c';import { x }\nfrom 'd';"
417428
(should (not (flyspell-predicate-test "'a")))
418429
(should (not (flyspell-predicate-test "'b")))
419430
(should (flyspell-predicate-test "'c"))
420431
(should (not (flyspell-predicate-test "'d"))))
421-
(test-with-temp-buffer
432+
(test-with-fontified-buffer
422433
;; This is valid TypeScript.
423434
"const from = 'a';"
424435
(should (flyspell-predicate-test "'a")))
425-
(test-with-temp-buffer
436+
(test-with-fontified-buffer
426437
;; TypeScript does not allow a function named "import" but object
427438
;; members may be named "import". So this *can* be valid
428439
;; TypeScript.
429440
"x.import('a');"
430441
(should (flyspell-predicate-test "'a")))))
431442

443+
444+
(ert-deftest typescript--move-to-end-of-plain-string ()
445+
"Unit tests for `typescript--move-to-end-of-plain-string'."
446+
(cl-flet
447+
((should-fail ()
448+
(let ((point-before (point)))
449+
(should (not (typescript--move-to-end-of-plain-string)))
450+
(should (eq (point) point-before))))
451+
(should-not-fail (expected)
452+
(let ((result (typescript--move-to-end-of-plain-string)))
453+
(should (eq result expected))
454+
(should (eq (point) expected)))))
455+
;;
456+
;; The tests below are structured as follows. For each case:
457+
;;
458+
;; 1. Move point to a new location in the buffer.
459+
;;
460+
;; 2. Check whether typescript--move-to-end-of-plain-string returns the value we expected
461+
;; and changes (point) when successful.
462+
;;
463+
;; Cases often start with a check right away: (point) equal to
464+
;; (point-min) for those cases.
465+
;;
466+
(dolist (delimiter '("'" "\""))
467+
(test-with-temp-buffer
468+
(replace-regexp-in-string "'" delimiter "const a = 'not terminated")
469+
(should-fail)
470+
(re-search-forward delimiter)
471+
(should-fail))
472+
(test-with-temp-buffer
473+
(replace-regexp-in-string "'" delimiter "const a = 'terminated'")
474+
(should-fail)
475+
;; This checks that the function works when invoked on the start delimiter of
476+
;; a terminated string.
477+
(re-search-forward delimiter)
478+
(should-not-fail (1- (point-max)))
479+
(goto-char (point-min))
480+
(re-search-forward "term")
481+
(should-not-fail (1- (point-max)))
482+
;; This checks that the function works when invoked on the end delimiter of
483+
;; a terminated string.
484+
(goto-char (1- (point-max)))
485+
(should-not-fail (1- (point-max))))
486+
(test-with-temp-buffer
487+
(replace-regexp-in-string "'" delimiter "const a = 'terminated aaa';\n
488+
const b = 'not terminated bbb")
489+
(should-fail)
490+
(re-search-forward "term")
491+
(should-not-fail (save-excursion (re-search-forward "aaa")))
492+
(re-search-forward "const b")
493+
(should-fail)
494+
(re-search-forward "not terminated")
495+
(should-fail))
496+
;; Case with escaped delimiter.
497+
(test-with-temp-buffer
498+
(replace-regexp-in-string "'" delimiter "const a = 'terminat\\'ed aaa';\n
499+
const b = 'not terminated bbb")
500+
(re-search-forward "term")
501+
(should-not-fail (save-excursion (re-search-forward "aaa"))))
502+
;; Delimiters in comments.
503+
(test-with-temp-buffer
504+
(replace-regexp-in-string "'" delimiter "const a = 'terminated aaa';\n
505+
// Comment 'or'\n
506+
const b = 'not terminated bbb")
507+
(re-search-forward "term")
508+
(should-not-fail (save-excursion (re-search-forward "aaa")))
509+
(re-search-forward "Comment ")
510+
(should-fail)
511+
(forward-char)
512+
(should-fail)
513+
(re-search-forward "or")
514+
(should-fail)))
515+
;; Ignores template strings.
516+
(test-with-temp-buffer
517+
"const a = `terminated aaa`"
518+
(re-search-forward "term")
519+
(should-fail))))
520+
521+
(ert-deftest typescript-convert-to-template ()
522+
"Unit tests for `typescript-convert-to-template'."
523+
(cl-flet
524+
((should-do-nothing (str regexp)
525+
(test-with-temp-buffer
526+
str
527+
(re-search-forward regexp)
528+
(typescript-convert-to-template)
529+
(should (string-equal (typescript-test-get-doc) str))))
530+
(should-modify (str delimiter regexp)
531+
(test-with-temp-buffer
532+
str
533+
(re-search-forward regexp)
534+
(typescript-convert-to-template)
535+
(should (string-equal (typescript-test-get-doc)
536+
(replace-regexp-in-string delimiter "`" str))))))
537+
(dolist (delimiter '("'" "\""))
538+
(let ((str (replace-regexp-in-string "'" delimiter "const a = 'not terminated")))
539+
(dolist (move-to '("const" "not"))
540+
(should-do-nothing str move-to)))
541+
(let ((str (replace-regexp-in-string "'" delimiter "const a = 'terminated'")))
542+
(should-do-nothing str "const")
543+
(should-modify str delimiter delimiter)
544+
(should-modify str delimiter "term")
545+
(should-modify str delimiter "terminated"))
546+
;; Delimiters in comments.
547+
(let ((str (replace-regexp-in-string "'" delimiter "const a = 'terminated aaa';\n
548+
// Comment 'or'\n
549+
const b = 'not terminated bbb")))
550+
(should-do-nothing str "Comment ")))
551+
;; Ignores template strings.
552+
(let ((str "const a = `terminated aaa`"))
553+
(should-do-nothing str "terminated"))))
554+
555+
(ert-deftest typescript-autoconvert-to-template ()
556+
"Unit tests for `typescript-autoconvert-to-template'."
557+
(cl-flet
558+
((should-do-nothing (str regexp)
559+
(test-with-temp-buffer
560+
str
561+
(re-search-forward regexp)
562+
(typescript-autoconvert-to-template)
563+
(should (string-equal (typescript-test-get-doc) str))))
564+
(should-modify (str delimiter regexp)
565+
(test-with-temp-buffer
566+
str
567+
(re-search-forward regexp)
568+
(typescript-autoconvert-to-template)
569+
(should (string-equal (typescript-test-get-doc)
570+
(replace-regexp-in-string delimiter "`" str))))))
571+
(dolist (delimiter '("'" "\""))
572+
(let ((str (replace-regexp-in-string "'" delimiter "const a = 'terminated'")))
573+
(should-do-nothing str "= ")
574+
(should-do-nothing str "terminated"))
575+
(let ((str (replace-regexp-in-string "'" delimiter "const a = '${foo}'")))
576+
(should-do-nothing str "= ")
577+
(should-modify str delimiter (concat "foo}" delimiter))))))
578+
579+
(ert-deftest typescript-autoconvert-to-template-is-invoked ()
580+
"Test that we call `typescript-autoconvert-to-template' as needed."
581+
(cl-flet
582+
((should-do-nothing (str delimiter)
583+
(test-with-temp-buffer
584+
str
585+
(goto-char (point-max))
586+
(execute-kbd-macro delimiter)
587+
(should (string-equal (typescript-test-get-doc) (concat str delimiter)))))
588+
(should-modify (str delimiter)
589+
(test-with-temp-buffer
590+
str
591+
(goto-char (point-max))
592+
(execute-kbd-macro delimiter)
593+
(should (string-equal (typescript-test-get-doc)
594+
(replace-regexp-in-string delimiter "`" (concat str delimiter)))))))
595+
(dolist (delimiter '("'" "\""))
596+
(let ((str (replace-regexp-in-string "'" delimiter "const a = '${foo}")))
597+
(should-do-nothing str delimiter)
598+
(let ((typescript-autoconvert-to-template-flag t))
599+
(should-modify str delimiter))))))
600+
432601
(provide 'typescript-mode-tests)
433602

434603
;;; typescript-mode-tests.el ends here

typescript-mode.el

+108-3
Original file line numberDiff line numberDiff line change
@@ -631,13 +631,72 @@ seldom use, either globally or on a per-buffer basis."
631631
:type 'hook
632632
:group 'typescript)
633633

634+
(defcustom typescript-autoconvert-to-template-flag nil
635+
"Non-nil means automatically convert plain strings to templates.
636+
637+
When the flag is non-nil the `typescript-autoconvert-to-template'
638+
is called whenever a plain string delimiter is typed in the buffer."
639+
:type 'boolean
640+
:group 'typescript)
641+
642+
;;; Public utilities
643+
644+
(defun typescript-convert-to-template ()
645+
"Convert the string at point to a template string."
646+
(interactive)
647+
(save-restriction
648+
(widen)
649+
(save-excursion
650+
(let* ((syntax (syntax-ppss))
651+
(str-terminator (nth 3 syntax))
652+
(string-start (or (and str-terminator (nth 8 syntax))
653+
;; We have to consider the case that we're on the start delimiter of a string.
654+
;; We tentatively take (point) as string-start. If it turns out we're
655+
;; wrong, then typescript--move-to-end-of-plain-string will fail anway,
656+
;; and we won't use the bogus value.
657+
(progn
658+
(forward-char)
659+
(point)))))
660+
(when (typescript--move-to-end-of-plain-string)
661+
(let ((end-start (or (nth 8 (syntax-ppss)) -1)))
662+
(undo-boundary)
663+
(when (= end-start string-start)
664+
(delete-char 1)
665+
(insert "`")))
666+
(goto-char string-start)
667+
(delete-char 1)
668+
(insert "`"))))))
669+
670+
(defun typescript-autoconvert-to-template ()
671+
"Automatically convert a plain string to a teplate string, if needed.
672+
673+
This function is meant to be automatically invoked when the user
674+
enters plain string delimiters. It checks whether the character
675+
before point is the end of a string. If it is, then it checks
676+
whether the string contains ${...}. If it does, then it converts
677+
the string from a plain string to a template."
678+
(interactive)
679+
(save-restriction
680+
(widen)
681+
(save-excursion
682+
(backward-char)
683+
(when (and (memq (char-after) '(?' ?\"))
684+
(not (eq (char-before) ?\\)))
685+
(let* ((string-start (nth 8 (syntax-ppss))))
686+
(when (and string-start
687+
(save-excursion
688+
(re-search-backward "\\${.*?}" string-start t)))
689+
(typescript-convert-to-template)))))))
690+
634691
;;; KeyMap
635692

636693
(defvar typescript-mode-map
637694
(let ((keymap (make-sparse-keymap)))
638-
(mapc (lambda (key)
639-
(define-key keymap key #'typescript-insert-and-indent))
640-
'("{" "}" "(" ")" ":" ";" ","))
695+
(dolist (key '("{" "}" "(" ")" ":" ";" ","))
696+
(define-key keymap key #'typescript-insert-and-indent))
697+
(dolist (key '("\"" "\'"))
698+
(define-key keymap key #'typescript-insert-and-autoconvert-to-template))
699+
(define-key keymap (kbd "C-c '") #'typescript-convert-to-template)
641700
keymap)
642701
"Keymap for `typescript-mode'.")
643702

@@ -655,6 +714,12 @@ comment."
655714
(1+ (current-indentation)))))
656715
(indent-according-to-mode))))
657716

717+
(defun typescript-insert-and-autoconvert-to-template (key)
718+
"Run the command bount to KEY, and autoconvert to template if necessary."
719+
(interactive (list (this-command-keys)))
720+
(call-interactively (lookup-key (current-global-map) key))
721+
(when typescript-autoconvert-to-template-flag
722+
(typescript-autoconvert-to-template)))
658723

659724
;;; Syntax table and parsing
660725

@@ -1490,6 +1555,46 @@ LIMIT defaults to point."
14901555
(when pitem
14911556
(goto-char (typescript--pitem-h-begin pitem )))))
14921557

1558+
(defun typescript--move-to-end-of-plain-string ()
1559+
"If the point is in a plain string, move to the end of it.
1560+
1561+
Otherwise, don't move. A plain string is a string which is not a
1562+
template string. The point is considered to be \"in\" a string if
1563+
it is on the delimiters of the string, or any point inside.
1564+
1565+
Returns point if the end of the string was found, or nil if the
1566+
end of the string was not found."
1567+
(let ((end-position
1568+
(save-excursion
1569+
(let* ((syntax (syntax-ppss))
1570+
(str-terminator (nth 3 syntax))
1571+
;; The 8th element will also be set if we are in a comment. So we
1572+
;; check str-terminator to protect against that.
1573+
(string-start (and str-terminator
1574+
(nth 8 syntax))))
1575+
(if (and string-start
1576+
(not (eq str-terminator ?`)))
1577+
;; We may already be at the end of the string.
1578+
(if (and (eq (char-after) str-terminator)
1579+
(not (eq (char-before) ?\\)))
1580+
(point)
1581+
;; We just search forward and then check if the hit we get has a
1582+
;; string-start equal to ours.
1583+
(loop while (re-search-forward
1584+
(concat "\\(?:[^\\]\\|^\\)\\(" (string str-terminator) "\\)")
1585+
nil t)
1586+
if (eq string-start
1587+
(save-excursion (nth 8 (syntax-ppss (match-beginning 1)))))
1588+
return (match-beginning 1)))
1589+
;; If we are on the start delimiter then the value of syntax-ppss will look
1590+
;; like we're not in a string at all, but this function considers the
1591+
;; start delimiter to be "in" the string. We take care of this here.
1592+
(when (memq (char-after) '(?' ?\"))
1593+
(forward-char)
1594+
(typescript--move-to-end-of-plain-string)))))))
1595+
(when end-position
1596+
(goto-char end-position))))
1597+
14931598
;;; Font Lock
14941599
(defun typescript--make-framework-matcher (framework &rest regexps)
14951600
"Helper function for building `typescript--font-lock-keywords'.

0 commit comments

Comments
 (0)