Skip to content

Commit

Permalink
Add function to convert plain strings to template strings.
Browse files Browse the repository at this point in the history
  • Loading branch information
lddubeau committed Apr 9, 2018
1 parent d850177 commit 5350c45
Show file tree
Hide file tree
Showing 2 changed files with 288 additions and 14 deletions.
191 changes: 180 additions & 11 deletions typescript-mode-tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

(require 'ert)
(require 'typescript-mode)
(require 'cl)

(defun typescript-test-get-doc ()
(buffer-substring-no-properties (point-min) (point-max)))
Expand Down Expand Up @@ -211,8 +212,18 @@ a severity set to WARNING, no rule name."
`(with-temp-buffer
(insert ,content)
(typescript-mode)
(font-lock-fontify-buffer)
(goto-char (point-min))
;; We need this so that tests that simulate user actions operate on the right buffer.
(switch-to-buffer (current-buffer))
,@body))

(defmacro test-with-fontified-buffer (content &rest body)
"Fill a temporary buffer with `CONTENT' and eval `BODY' in it."
(declare (debug t)
(indent 1))
`(test-with-temp-buffer
,content
(font-lock-fontify-buffer)
,@body))

(defun get-face-at (loc)
Expand All @@ -234,7 +245,7 @@ put in the temporary buffer. `EXPECTED' is the expected
results. It should be a list of (LOCATION . FACE) pairs, where
LOCATION can be either a single location, or list of locations,
that are all expected to have the same face."
(test-with-temp-buffer
(test-with-fontified-buffer
contents
;; Make sure our propertize function has been applied to the whole
;; buffer.
Expand Down Expand Up @@ -263,7 +274,7 @@ documentation."
(ert-deftest font-lock/no-documentation-in-non-documentation-comments ()
"Documentation tags that are not in documentation comments
should not be fontified as documentation."
(test-with-temp-buffer
(test-with-fontified-buffer
(concat "/*\n" font-lock-contents "\n*/\n")
(let ((loc 3))
;; Make sure we start with the right face.
Expand All @@ -274,7 +285,7 @@ should not be fontified as documentation."
(ert-deftest font-lock/no-documentation-in-strings ()
"Documentation tags that are not in strings should not be
fontified as documentation."
(test-with-temp-buffer
(test-with-fontified-buffer
(concat "const x = \"/**" font-lock-contents "*/\";")
(let ((loc (search-forward "\"")))
;; Make sure we start with the right face.
Expand Down Expand Up @@ -374,16 +385,16 @@ declare function declareFunctionDefn(x3: xty3, y3: yty3): ret3;"

(ert-deftest font-lock/text-after-trailing-regexp-delim-should-not-be-fontified ()
"Text after trailing regular expression delimiter should not be fontified."
(test-with-temp-buffer
(test-with-fontified-buffer
"=/foo/g something // comment"
(should (eq (get-face-at "g something") nil)))
(test-with-temp-buffer
(test-with-fontified-buffer
"=/foo\\bar/g something // comment"
(should (eq (get-face-at "g something") nil)))
(test-with-temp-buffer
(test-with-fontified-buffer
"=/foo\\\\bar/g something // comment"
(should (eq (get-face-at "g something") nil)))
(test-with-temp-buffer
(test-with-fontified-buffer
"=/foo\\\\/g something // comment"
(should (eq (get-face-at "g something") nil))))

Expand Down Expand Up @@ -412,23 +423,181 @@ import... from...."
;; to avoid hitting keywords. Moreover, the end position of the search is important.
;; Flyspell puts point at the end of the word before calling the predicate. We must
;; replicate that behavior here.
(test-with-temp-buffer
(test-with-fontified-buffer
"import 'a';\nimport { x } from 'b';\nconst foo = 'c';import { x }\nfrom 'd';"
(should (not (flyspell-predicate-test "'a")))
(should (not (flyspell-predicate-test "'b")))
(should (flyspell-predicate-test "'c"))
(should (not (flyspell-predicate-test "'d"))))
(test-with-temp-buffer
(test-with-fontified-buffer
;; This is valid TypeScript.
"const from = 'a';"
(should (flyspell-predicate-test "'a")))
(test-with-temp-buffer
(test-with-fontified-buffer
;; TypeScript does not allow a function named "import" but object
;; members may be named "import". So this *can* be valid
;; TypeScript.
"x.import('a');"
(should (flyspell-predicate-test "'a")))))


(ert-deftest typescript--move-to-end-of-plain-string ()
"Unit tests for `typescript--move-to-end-of-plain-string'."
(cl-flet
((should-fail ()
(let ((point-before (point)))
(should (not (typescript--move-to-end-of-plain-string)))
(should (eq (point) point-before))))
(should-not-fail (expected)
(let ((result (typescript--move-to-end-of-plain-string)))
(should (eq result expected))
(should (eq (point) expected)))))
;;
;; The tests below are structured as follows. For each case:
;;
;; 1. Move point to a new location in the buffer.
;;
;; 2. Check whether typescript--move-to-end-of-plain-string returns the value we expected
;; and changes (point) when successful.
;;
;; Cases often start with a check right away: (point) equal to
;; (point-min) for those cases.
;;
(dolist (delimiter '("'" "\""))
(test-with-temp-buffer
(replace-regexp-in-string "'" delimiter "const a = 'not terminated")
(should-fail)
(re-search-forward delimiter)
(should-fail))
(test-with-temp-buffer
(replace-regexp-in-string "'" delimiter "const a = 'terminated'")
(should-fail)
;; This checks that the function works when invoked on the start delimiter of
;; a terminated string.
(re-search-forward delimiter)
(should-not-fail (1- (point-max)))
(goto-char (point-min))
(re-search-forward "term")
(should-not-fail (1- (point-max)))
;; This checks that the function works when invoked on the end delimiter of
;; a terminated string.
(goto-char (1- (point-max)))
(should-not-fail (1- (point-max))))
(test-with-temp-buffer
(replace-regexp-in-string "'" delimiter "const a = 'terminated aaa';\n
const b = 'not terminated bbb")
(should-fail)
(re-search-forward "term")
(should-not-fail (save-excursion (re-search-forward "aaa")))
(re-search-forward "const b")
(should-fail)
(re-search-forward "not terminated")
(should-fail))
;; Case with escaped delimiter.
(test-with-temp-buffer
(replace-regexp-in-string "'" delimiter "const a = 'terminat\\'ed aaa';\n
const b = 'not terminated bbb")
(re-search-forward "term")
(should-not-fail (save-excursion (re-search-forward "aaa"))))
;; Delimiters in comments.
(test-with-temp-buffer
(replace-regexp-in-string "'" delimiter "const a = 'terminated aaa';\n
// Comment 'or'\n
const b = 'not terminated bbb")
(re-search-forward "term")
(should-not-fail (save-excursion (re-search-forward "aaa")))
(re-search-forward "Comment ")
(should-fail)
(forward-char)
(should-fail)
(re-search-forward "or")
(should-fail)))
;; Ignores template strings.
(test-with-temp-buffer
"const a = `terminated aaa`"
(re-search-forward "term")
(should-fail))))

(ert-deftest typescript-convert-to-template ()
"Unit tests for `typescript-convert-to-template'."
(cl-flet
((should-do-nothing (str regexp)
(test-with-temp-buffer
str
(re-search-forward regexp)
(typescript-convert-to-template)
(should (string-equal (typescript-test-get-doc) str))))
(should-modify (str delimiter regexp)
(test-with-temp-buffer
str
(re-search-forward regexp)
(typescript-convert-to-template)
(should (string-equal (typescript-test-get-doc)
(replace-regexp-in-string delimiter "`" str))))))
(dolist (delimiter '("'" "\""))
(let ((str (replace-regexp-in-string "'" delimiter "const a = 'not terminated")))
(dolist (move-to '("const" "not"))
(should-do-nothing str move-to)))
(let ((str (replace-regexp-in-string "'" delimiter "const a = 'terminated'")))
(should-do-nothing str "const")
(should-modify str delimiter delimiter)
(should-modify str delimiter "term")
(should-modify str delimiter "terminated"))
;; Delimiters in comments.
(let ((str (replace-regexp-in-string "'" delimiter "const a = 'terminated aaa';\n
// Comment 'or'\n
const b = 'not terminated bbb")))
(should-do-nothing str "Comment ")))
;; Ignores template strings.
(let ((str "const a = `terminated aaa`"))
(should-do-nothing str "terminated"))))

(ert-deftest typescript-autoconvert-to-template ()
"Unit tests for `typescript-autoconvert-to-template'."
(cl-flet
((should-do-nothing (str regexp)
(test-with-temp-buffer
str
(re-search-forward regexp)
(typescript-autoconvert-to-template)
(should (string-equal (typescript-test-get-doc) str))))
(should-modify (str delimiter regexp)
(test-with-temp-buffer
str
(re-search-forward regexp)
(typescript-autoconvert-to-template)
(should (string-equal (typescript-test-get-doc)
(replace-regexp-in-string delimiter "`" str))))))
(dolist (delimiter '("'" "\""))
(let ((str (replace-regexp-in-string "'" delimiter "const a = 'terminated'")))
(should-do-nothing str "= ")
(should-do-nothing str "terminated"))
(let ((str (replace-regexp-in-string "'" delimiter "const a = '${foo}'")))
(should-do-nothing str "= ")
(should-modify str delimiter (concat "foo}" delimiter))))))

(ert-deftest typescript-autoconvert-to-template-is-invoked ()
"Test that we call `typescript-autoconvert-to-template' as needed."
(cl-flet
((should-do-nothing (str delimiter)
(test-with-temp-buffer
str
(goto-char (point-max))
(execute-kbd-macro delimiter)
(should (string-equal (typescript-test-get-doc) (concat str delimiter)))))
(should-modify (str delimiter)
(test-with-temp-buffer
str
(goto-char (point-max))
(execute-kbd-macro delimiter)
(should (string-equal (typescript-test-get-doc)
(replace-regexp-in-string delimiter "`" (concat str delimiter)))))))
(dolist (delimiter '("'" "\""))
(let ((str (replace-regexp-in-string "'" delimiter "const a = '${foo}")))
(should-do-nothing str delimiter)
(let ((typescript-autoconvert-to-template-flag t))
(should-modify str delimiter))))))

(provide 'typescript-mode-tests)

;;; typescript-mode-tests.el ends here
111 changes: 108 additions & 3 deletions typescript-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -631,13 +631,72 @@ seldom use, either globally or on a per-buffer basis."
:type 'hook
:group 'typescript)

(defcustom typescript-autoconvert-to-template-flag nil
"Non-nil means automatically convert plain strings to templates.
When the flag is non-nil the `typescript-autoconvert-to-template'
is called whenever a plain string delimiter is typed in the buffer."
:type 'boolean
:group 'typescript)

;;; Public utilities

(defun typescript-convert-to-template ()
"Convert the string at point to a template string."
(interactive)
(save-restriction
(widen)
(save-excursion
(let* ((syntax (syntax-ppss))
(str-terminator (nth 3 syntax))
(string-start (or (and str-terminator (nth 8 syntax))
;; We have to consider the case that we're on the start delimiter of a string.
;; We tentatively take (point) as string-start. If it turns out we're
;; wrong, then typescript--move-to-end-of-plain-string will fail anway,
;; and we won't use the bogus value.
(progn
(forward-char)
(point)))))
(when (typescript--move-to-end-of-plain-string)
(let ((end-start (or (nth 8 (syntax-ppss)) -1)))
(undo-boundary)
(when (= end-start string-start)
(delete-char 1)
(insert "`")))
(goto-char string-start)
(delete-char 1)
(insert "`"))))))

(defun typescript-autoconvert-to-template ()
"Automatically convert a plain string to a teplate string, if needed.
This function is meant to be automatically invoked when the user
enters plain string delimiters. It checks whether the character
before point is the end of a string. If it is, then it checks
whether the string contains ${...}. If it does, then it converts
the string from a plain string to a template."
(interactive)
(save-restriction
(widen)
(save-excursion
(backward-char)
(when (and (memq (char-after) '(?' ?\"))
(not (eq (char-before) ?\\)))
(let* ((string-start (nth 8 (syntax-ppss))))
(when (and string-start
(save-excursion
(re-search-backward "\\${.*?}" string-start t)))
(typescript-convert-to-template)))))))

;;; KeyMap

(defvar typescript-mode-map
(let ((keymap (make-sparse-keymap)))
(mapc (lambda (key)
(define-key keymap key #'typescript-insert-and-indent))
'("{" "}" "(" ")" ":" ";" ","))
(dolist (key '("{" "}" "(" ")" ":" ";" ","))
(define-key keymap key #'typescript-insert-and-indent))
(dolist (key '("\"" "\'"))
(define-key keymap key #'typescript-insert-and-autoconvert-to-template))
(define-key keymap (kbd "C-c '") #'typescript-convert-to-template)
keymap)
"Keymap for `typescript-mode'.")

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

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

;;; Syntax table and parsing

Expand Down Expand Up @@ -1490,6 +1555,46 @@ LIMIT defaults to point."
(when pitem
(goto-char (typescript--pitem-h-begin pitem )))))

(defun typescript--move-to-end-of-plain-string ()
"If the point is in a plain string, move to the end of it.
Otherwise, don't move. A plain string is a string which is not a
template string. The point is considered to be \"in\" a string if
it is on the delimiters of the string, or any point inside.
Returns point if the end of the string was found, or nil if the
end of the string was not found."
(let ((end-position
(save-excursion
(let* ((syntax (syntax-ppss))
(str-terminator (nth 3 syntax))
;; The 8th element will also be set if we are in a comment. So we
;; check str-terminator to protect against that.
(string-start (and str-terminator
(nth 8 syntax))))
(if (and string-start
(not (eq str-terminator ?`)))
;; We may already be at the end of the string.
(if (and (eq (char-after) str-terminator)
(not (eq (char-before) ?\\)))
(point)
;; We just search forward and then check if the hit we get has a
;; string-start equal to ours.
(loop while (re-search-forward
(concat "\\(?:[^\\]\\|^\\)\\(" (string str-terminator) "\\)")
nil t)
if (eq string-start
(save-excursion (nth 8 (syntax-ppss (match-beginning 1)))))
return (match-beginning 1)))
;; If we are on the start delimiter then the value of syntax-ppss will look
;; like we're not in a string at all, but this function considers the
;; start delimiter to be "in" the string. We take care of this here.
(when (memq (char-after) '(?' ?\"))
(forward-char)
(typescript--move-to-end-of-plain-string)))))))
(when end-position
(goto-char end-position))))

;;; Font Lock
(defun typescript--make-framework-matcher (framework &rest regexps)
"Helper function for building `typescript--font-lock-keywords'.
Expand Down

0 comments on commit 5350c45

Please sign in to comment.