Skip to content

Commit 064d47b

Browse files
authored
Merge pull request #1 from emacs-lsp/add-support-for-tests
Add test code lens support
2 parents 8289f30 + 4cce8b7 commit 064d47b

File tree

2 files changed

+194
-2
lines changed

2 files changed

+194
-2
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ Besides the `lsp-mode` features, `lsp-dart` implements the [custom methods featu
4646

4747
![flutter-outline](https://raw.githubusercontent.com/emacs-lsp/lsp-dart/screenshots/flutter-outline.gif)
4848

49+
### Run tests
50+
51+
`lsp-dart-run-test-file` - Run all tests from current test buffer.
52+
53+
`lsp-dart-run-test-at-point` - Run single test at point.
54+
55+
Running a test interactively:
56+
57+
![test](https://raw.githubusercontent.com/emacs-lsp/lsp-dart/screenshots/run-test.gif)
58+
4959
## Supported settings
5060

5161
* `lsp-dart-sdk-dir` - Install directory for dart-sdk.

lsp-dart.el

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ in the PATH env."
4444
:risky t
4545
:type '(choice directory nil))
4646

47+
(defcustom lsp-dart-flutter-command "flutter"
48+
"Flutter command for running tests."
49+
:group 'lsp-dart
50+
:type 'string)
51+
4752
(defcustom lsp-dart-server-command nil
4853
"The analysis_server executable to use."
4954
:type '(repeat string)
@@ -112,17 +117,24 @@ Defaults to side following treemacs default."
112117
:type 'list
113118
:group 'lsp-dart)
114119

120+
(defcustom lsp-dart-test-code-lens t
121+
"Enable the test code lens overlays."
122+
:type 'boolean
123+
:group 'lsp-dart)
124+
115125

116126
;;; Internal
117127

118128
(defun lsp-dart--find-sdk-dir ()
119129
"Find dart sdk by searching for dart executable or flutter cache dir."
120130
(-when-let (dart (or (executable-find "dart")
121-
(-when-let (flutter (executable-find "flutter"))
131+
(-when-let (flutter (-> lsp-dart-flutter-command
132+
executable-find
133+
file-truename))
122134
(expand-file-name "cache/dart-sdk/bin/dart"
123135
(file-name-directory flutter)))))
124136
(-> dart
125-
(file-truename)
137+
file-truename
126138
(locate-dominating-file "bin"))))
127139

128140
(defun lsp-dart--outline-kind->icon (kind)
@@ -268,6 +280,9 @@ Focus on it if IGNORE-FOCUS? is nil."
268280
PARAMS outline notification data sent from WORKSPACE.
269281
It updates the outline view if it already exists."
270282
(lsp-workspace-set-metadata "current-outline" params workspace)
283+
(when (and lsp-dart-test-code-lens
284+
(lsp-dart-test-file-p (gethash "uri" params)))
285+
(lsp-dart-check-test-code-lens params))
271286
(when (get-buffer-window "*Dart Outline*")
272287
(lsp-dart--show-outline t)))
273288

@@ -326,6 +341,146 @@ PARAMS closing labels notification data sent from WORKSPACE."
326341
("dart/textDocument/publishFlutterOutline" 'lsp-dart--handle-flutter-outline))
327342
:server-id 'dart_analysis_server))
328343

344+
;;; test
345+
346+
(defun lsp-dart--test-method-p (kind)
347+
"Return non-nil if KIND is a test type."
348+
(or (string= kind "UNIT_TEST_TEST")
349+
(string= kind "UNIT_TEST_GROUP")))
350+
351+
(defun lsp-dart--test-flutter-test-file-p (buffer)
352+
"Return non-nil if the BUFFER appears to be a flutter test file."
353+
(with-current-buffer buffer
354+
(save-excursion
355+
(goto-char (point-min))
356+
(re-search-forward "^import 'package:flutter_test/flutter_test.dart';"
357+
nil t))))
358+
359+
(defun lsp-dart--last-index-of (regex str &optional ignore-case)
360+
"Find the last index of a REGEX in a string STR.
361+
IGNORE-CASE is a optional arg to ignore the case sensitive on regex search."
362+
(let ((start 0)
363+
(case-fold-search ignore-case)
364+
idx)
365+
(while (string-match regex str start)
366+
(setq idx (match-beginning 0))
367+
(setq start (match-end 0)))
368+
idx))
369+
370+
(defun lsp-dart--test-get-project-root ()
371+
"Return the dart or flutter project root."
372+
(locate-dominating-file default-directory "pubspec.yaml") )
373+
374+
(defmacro lsp-dart--test-from-project-root (&rest body)
375+
"Execute BODY with cwd set to the project root."
376+
`(let ((project-root (lsp-dart--test-get-project-root)))
377+
(if project-root
378+
(let ((default-directory project-root))
379+
,@body)
380+
(error "Dart or Flutter project not found (pubspec.yaml not found)"))))
381+
382+
(defun lsp-dart--build-command (buffer)
383+
"Build the dart or flutter build command.
384+
If the given BUFFER is a flutter test file, return the flutter command
385+
otherwise the dart command."
386+
(let ((sdk-dir (or lsp-dart-sdk-dir (lsp-dart--find-sdk-dir))))
387+
(if (lsp-dart--test-flutter-test-file-p buffer)
388+
lsp-dart-flutter-command
389+
(concat (file-name-as-directory sdk-dir) "bin/pub run"))))
390+
391+
(defun lsp-dart--build-test-name (names)
392+
"Build the test name from a group of test NAMES."
393+
(when (and names
394+
(not (seq-empty-p names)))
395+
(->> names
396+
(--map (substring it
397+
(+ (cl-search "(" it) 2)
398+
(- (lsp-dart--last-index-of ")" it) 1)))
399+
(--reduce (format "%s %s" acc it)))))
400+
401+
(defun lsp-dart--escape-test-name (name)
402+
"Return the dart safe escaped test NAME."
403+
(let ((escaped-str (regexp-quote name)))
404+
(seq-doseq (char '("(" ")" "{" "}"))
405+
(setq escaped-str (replace-regexp-in-string char
406+
(concat "\\" char)
407+
escaped-str nil t)))
408+
escaped-str))
409+
410+
(defun lsp-dart--run-test (buffer &optional names kind)
411+
"Run Dart/Flutter test command in a compilation buffer for BUFFER file.
412+
If NAMES is non nil, it will run only for KIND the test joining the name
413+
from NAMES."
414+
(interactive)
415+
(lsp-dart--test-from-project-root
416+
(let* ((test-file (file-relative-name (buffer-file-name buffer)
417+
(lsp-dart--test-get-project-root)))
418+
(test-name (lsp-dart--build-test-name names))
419+
(group-kind? (string= kind "UNIT_TEST_GROUP"))
420+
(test-arg (when test-name
421+
(concat "--name '^"
422+
(lsp-dart--escape-test-name test-name)
423+
(if group-kind? "'" "$'")))))
424+
(compilation-start (format "%s test %s %s"
425+
(lsp-dart--build-command buffer)
426+
(or test-arg "")
427+
test-file)
428+
t))))
429+
430+
(defun lsp-dart--build-test-overlay (buffer names kind range test-range)
431+
"Build an overlay for a test NAMES of KIND in BUFFER file.
432+
RANGE is the overlay range to build."
433+
(-let* ((beg-position (gethash "character" (gethash "start" range)))
434+
((beg . end) (lsp--range-to-region range))
435+
(beg-line (progn (goto-char beg)
436+
(line-beginning-position)))
437+
(spaces (make-string beg-position ?\s))
438+
(overlay (make-overlay beg-line end buffer)))
439+
(overlay-put overlay 'lsp-dart-test-code-lens t)
440+
(overlay-put overlay 'lsp-dart-test-names names)
441+
(overlay-put overlay 'lsp-dart-test-kind kind)
442+
(overlay-put overlay 'lsp-dart-test-overlay-test-range (lsp--range-to-region test-range))
443+
(overlay-put overlay 'before-string
444+
(concat spaces
445+
(propertize "Run\n"
446+
'help-echo "mouse-1: Run this test"
447+
'mouse-face 'lsp-lens-mouse-face
448+
'local-map (-doto (make-sparse-keymap)
449+
(define-key [mouse-1] (lambda ()
450+
(interactive)
451+
(lsp-dart--run-test buffer names kind))))
452+
'font-lock-face 'lsp-lens-face)))))
453+
454+
(defun lsp-dart--add-test-code-lens (buffer items &optional names)
455+
"Add test code lens to BUFFER for ITEMS.
456+
NAMES arg is optional and are the group of tests representing a test name."
457+
(seq-doseq (item items)
458+
(-let* (((&hash "children" "codeRange" test-range "element"
459+
(&hash "kind" "name" "range")) item)
460+
(test-kind? (lsp-dart--test-method-p kind))
461+
(concatened-names (if test-kind?
462+
(append names (list name))
463+
names)))
464+
(when test-kind?
465+
(lsp-dart--build-test-overlay buffer (append names (list name)) kind range test-range))
466+
(unless (seq-empty-p children)
467+
(lsp-dart--add-test-code-lens buffer children concatened-names)))))
468+
469+
(defun lsp-dart-test-file-p (file-name)
470+
"Return non-nil if FILE-NAME is a dart test files."
471+
(string-match "_test.dart" file-name))
472+
473+
(defun lsp-dart-check-test-code-lens (params)
474+
"Check for test adding lens to it.
475+
PARAMS is the notification data from outline."
476+
(-let* (((&hash "uri" "outline" (&hash "children")) params)
477+
(buffer (lsp--buffer-for-file (lsp--uri-to-path uri))))
478+
(when buffer
479+
(with-current-buffer buffer
480+
(remove-overlays (point-min) (point-max) 'lsp-dart-test-code-lens t)
481+
(save-excursion
482+
(lsp-dart--add-test-code-lens buffer children))))))
483+
329484

330485
;;; Public interface
331486

@@ -341,6 +496,33 @@ PARAMS closing labels notification data sent from WORKSPACE."
341496
(interactive "P")
342497
(lsp-dart--show-flutter-outline ignore-focus?))
343498

499+
;;;###autoload
500+
(defun lsp-dart-run-test-at-point ()
501+
"Run test checking for the previous overlay at point.
502+
Run test of the overlay which has the smallest range of
503+
all test overlays in the current buffer."
504+
(interactive)
505+
(-some--> (overlays-in (point-min) (point-max))
506+
(--filter (when (overlay-get it 'lsp-dart-test-code-lens)
507+
(-let* (((beg . end) (overlay-get it 'lsp-dart-test-overlay-test-range)))
508+
(and (>= (point) beg)
509+
(<= (point) end)))) it)
510+
(--min-by (-let* (((beg1 . end1) (overlay-get it 'lsp-dart-test-overlay-test-range))
511+
((beg2 . end2) (overlay-get other 'lsp-dart-test-overlay-test-range)))
512+
(and (< beg1 beg2)
513+
(> end1 end2))) it)
514+
(lsp-dart--run-test (current-buffer)
515+
(overlay-get it 'lsp-dart-test-names)
516+
(overlay-get it 'lsp-dart-test-kind))))
517+
518+
;;;###autoload
519+
(defun lsp-dart-run-test-file ()
520+
"Run dart/Flutter test command only for current buffer."
521+
(interactive)
522+
(if (lsp-dart-test-file-p (buffer-file-name))
523+
(lsp-dart--run-test (current-buffer))
524+
(user-error "Current buffer is not a Dart/Flutter test file")))
525+
344526

345527
;;;###autoload(with-eval-after-load 'lsp-mode (require 'lsp-dart))
346528

0 commit comments

Comments
 (0)