@@ -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."
268280PARAMS outline notification data sent from WORKSPACE.
269281It 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