Skip to content

Commit 3d04acf

Browse files
tninjaKang Tu
andauthored
Chore: Improve ai-code-implement-todo (#323)
* Reorder auto-test choices to prioritize no-test option * Add org-mode TODO headline detection to detect-todo-info * Route ask-question to implement-todo on TODO comments * Default action to skip completing-read in implement-todo * fix: Address PR review feedback and fix pre-existing test failures Add explicit (require 'ai-code-change) to ai-code-discussion.el and gate TODO routing on buffer-file-name to prevent errors in unsaved buffers. Fix claude-code multiline test by mocking MCP agent launch, and fix capf test by adding ai-code-git require and magit-git-lines mock to test repo macro. --------- Co-authored-by: Kang Tu <kang_tu@apple.com>
1 parent be79e6f commit 3d04acf

7 files changed

Lines changed: 287 additions & 28 deletions

ai-code-change.el

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,35 @@ returns that function's name. Otherwise returns the result of `which-function`.
124124
"Detect TODO comment information at cursor or in selected region.
125125
REGION-ACTIVE indicates whether a region is selected.
126126
Returns (TEXT START-POS END-POS) if TODO found, nil otherwise."
127+
;; DONE: this function should include org-mode TODO headline detection as well, as it used inside ai-code-implement-todo.
127128
(let ((text (if region-active
128129
(buffer-substring-no-properties (region-beginning) (region-end))
129130
(thing-at-point 'line t))))
130-
(when (and text comment-start)
131-
(let* ((first-line (car (split-string text "\n")))
132-
(comment-prefix-re (concat "^[ \t]*" (regexp-quote (string-trim-right comment-start)) "+[ \t]*")))
133-
(when (string-match comment-prefix-re first-line)
134-
(let ((rest (string-trim-left (substring first-line (match-end 0)))))
135-
(when (string-prefix-p "TODO" rest)
136-
(list text
137-
(if region-active (region-beginning) (line-beginning-position))
138-
(if region-active (region-end) (line-end-position))))))))))
131+
(or
132+
(when (and text comment-start)
133+
(let* ((first-line (car (split-string text "\n")))
134+
(comment-prefix-re (concat "^[ \t]*" (regexp-quote (string-trim-right comment-start)) "+[ \t]*")))
135+
(when (string-match comment-prefix-re first-line)
136+
(let ((rest (string-trim-left (substring first-line (match-end 0)))))
137+
(when (string-prefix-p "TODO" rest)
138+
(list text
139+
(if region-active (region-beginning) (line-beginning-position))
140+
(if region-active (region-end) (line-end-position))))))))
141+
(when (and (not region-active)
142+
(derived-mode-p 'org-mode)
143+
text)
144+
(save-excursion
145+
(when (and (org-at-heading-p)
146+
(ignore-errors (org-back-to-heading t)))
147+
(let ((heading-line (buffer-substring-no-properties
148+
(line-beginning-position)
149+
(line-end-position))))
150+
(when (and (or (org-get-todo-state)
151+
(ai-code--implement-todo--org-todo-headline-p heading-line))
152+
(not (org-entry-is-done-p)))
153+
(list heading-line
154+
(line-beginning-position)
155+
(line-end-position))))))))))
139156

140157
(defun ai-code--generate-prompt-label (clipboard-context region-active function-name)
141158
"Generate appropriate prompt label based on context.
@@ -249,11 +266,12 @@ Argument ARG is the prefix argument."
249266
(let* ((region-active (region-active-p))
250267
(todo-info (ai-code--detect-todo-info region-active)))
251268
(if todo-info
252-
(ai-code-implement-todo arg)
269+
;; DONE: in this code path, is it possible to default to Code Change in ai-code-implement-todo, no need to ask user to choose Code Change or Ask Question?
270+
(ai-code-implement-todo arg "Code change")
253271
(ai-code--handle-regular-code-change arg region-active))))))
254272

255273
;;;###autoload
256-
(defun ai-code-implement-todo (arg)
274+
(defun ai-code-implement-todo (arg &optional default-action)
257275
"Generate prompt to implement TODO comments in current context.
258276
Implements code after TODO comments instead of replacing them in-place.
259277
With a prefix argument \\[universal-argument], append the clipboard
@@ -264,7 +282,8 @@ The input string will be prefixed with TODO: and insert to the current
264282
line, with proper indentation. If cursor is inside a function, implement
265283
comments for that function.
266284
Otherwise implement comments for the entire current file.
267-
Argument ARG is the prefix argument."
285+
Argument ARG is the prefix argument.
286+
Optional DEFAULT-ACTION skips the action prompt when non-nil."
268287
;; DONE: I want to implement the idea inside https://github.com/tninja/ai-code-interface.el/issues/316, it could to either code change or ask question, given user's input with completing-read selection. The difference of this org-mode section TODO, with the existing comment todo is, it won't replace the TODO section with implementation. It just use the section headline and content inside this section as part of prompt, and send to AI.
269288
(interactive "P")
270289
(if (not buffer-file-name)
@@ -274,7 +293,7 @@ Argument ARG is the prefix argument."
274293
(cl-return-from finalize nil))
275294
(when (ai-code--implement-todo--handle-blank-line)
276295
(cl-return-from finalize nil))
277-
(ai-code--implement-todo--build-and-send-prompt arg))))
296+
(ai-code--implement-todo--build-and-send-prompt arg default-action))))
278297

279298
(defun ai-code--implement-todo--handle-done-line ()
280299
"Handle actions when current line is a DONE comment.
@@ -378,9 +397,10 @@ The plist contains `:heading-line', `:content', and `:line-number'."
378397
(string-match-p "^[[:space:]]*\\*+[[:space:]]+TODO:?\\(?:[[:space:]]\\|$\\)"
379398
heading-line))
380399

381-
(defun ai-code--implement-todo--build-and-send-prompt (arg)
400+
(defun ai-code--implement-todo--build-and-send-prompt (arg &optional default-action)
382401
"Build the TODO implementation prompt and insert it.
383-
ARG is the prefix argument for clipboard context."
402+
ARG is the prefix argument for clipboard context.
403+
Optional DEFAULT-ACTION skips the completing-read prompt when non-nil."
384404
;; DONE: ask user with completing-read before build up prompt, candidate should be 1. Code change; 2. Ask question. Given selection, add suffix to them respectively to indicate AI to make code change, or do not make any code change
385405
(let* ((clipboard-context (when arg (ai-code--get-clipboard-text)))
386406
(current-line (string-trim (thing-at-point 'line t)))
@@ -423,9 +443,10 @@ ARG is the prefix argument for clipboard context."
423443
(user-error "Current line is not a TODO comment or Org TODO headline and cannot proceed with `ai-code-implement-todo'. Please select a TODO comment (not DONE), an Org TODO headline, a region of comments, or activate on a blank line")))
424444
(_ (unless region-comment-block-p
425445
(user-error "Selected region must be a comment block")))
426-
(action-intent (completing-read "Select action: "
427-
'("Code change" "Ask question")
428-
nil t))
446+
(action-intent (or default-action
447+
(completing-read "Select action: "
448+
'("Code change" "Ask question")
449+
nil t)))
429450
(ask-question-p (string= action-intent "Ask question"))
430451
(prompt-label
431452
(cond

ai-code-discussion.el

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
(require 'ai-code-input)
1515
(require 'ai-code-prompt-mode)
16+
(require 'ai-code-change)
1617

1718
(declare-function ai-code-read-string "ai-code-input")
1819
(declare-function ai-code--insert-prompt "ai-code-prompt-mode")
@@ -102,13 +103,18 @@ Inserts the prompt into the AI prompt file and optionally sends to AI.
102103
103104
Argument ARG is the prefix argument."
104105
(interactive "P")
105-
(let ((clipboard-context (when arg (ai-code--get-clipboard-text))))
106-
(cond
107-
;; Handle dired buffer
108-
((derived-mode-p 'dired-mode)
109-
(ai-code--ask-question-dired clipboard-context))
110-
;; Handle regular file buffer
111-
(t (ai-code--ask-question-file clipboard-context)))))
106+
;; DONE: similar to ai-code-code-change, when todo-info is available, call ai-code-implement-todo
107+
(cond
108+
((derived-mode-p 'dired-mode)
109+
(let ((clipboard-context (when arg (ai-code--get-clipboard-text))))
110+
(ai-code--ask-question-dired clipboard-context)))
111+
(t
112+
(let ((todo-info (when buffer-file-name
113+
(ai-code--detect-todo-info (region-active-p)))))
114+
(if todo-info
115+
(ai-code-implement-todo arg "Ask question")
116+
(let ((clipboard-context (when arg (ai-code--get-clipboard-text))))
117+
(ai-code--ask-question-file clipboard-context)))))))
112118

113119
(defun ai-code--ask-question-dired (clipboard-context)
114120
"Handle ask question for Dired buffer.

ai-code.el

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,9 @@ See the later `defcustom' for user-facing documentation and default.")
182182

183183
(defconst ai-code--auto-test-type-ask-choices
184184
'(("Run tests after code change" . test-after-change)
185+
("Do not write or run tests" . no-test)
185186
("TDD Red + Green (write failing test, then make it pass)" . tdd)
186-
("TDD Red + Green + Blue (refactor after Green)" . tdd-with-refactoring)
187-
("Do not write or run tests" . no-test))
187+
("TDD Red + Green + Blue (refactor after Green)" . tdd-with-refactoring))
188188
"Resolve auto test suffix choices for `ask-me` mode.")
189189

190190
(defconst ai-code--auto-test-type-persistent-choices

test/test_ai-code-change.el

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,146 @@ is between the function definition and its body."
581581
(should (string-match-p "\\*\\* TODO: what is the most important verse in Bible"
582582
captured-prompt))))))
583583

584+
(ert-deftest ai-code-test-detect-todo-info-org-todo-headline ()
585+
"Test `ai-code--detect-todo-info' detects Org TODO headlines."
586+
(with-temp-buffer
587+
(require 'org)
588+
(setq buffer-file-name "plan.org")
589+
(insert "* TODO Build search feature\n")
590+
(insert "Design the API first.\n")
591+
(org-mode)
592+
(goto-char (point-min))
593+
594+
(cl-letf (((symbol-function 'region-active-p) (lambda () nil)))
595+
(let ((result (ai-code--detect-todo-info nil)))
596+
(should result)
597+
(should (stringp (nth 0 result)))
598+
(should (string-match-p "TODO Build search feature" (nth 0 result)))
599+
(should (integerp (nth 1 result)))
600+
(should (integerp (nth 2 result)))))))
601+
602+
(ert-deftest ai-code-test-detect-todo-info-org-done-headline-returns-nil ()
603+
"Test `ai-code--detect-todo-info' returns nil for Org DONE headlines."
604+
(with-temp-buffer
605+
(require 'org)
606+
(setq buffer-file-name "plan.org")
607+
(insert "* DONE Completed task\n")
608+
(org-mode)
609+
(goto-char (point-min))
610+
611+
(cl-letf (((symbol-function 'region-active-p) (lambda () nil)))
612+
(should-not (ai-code--detect-todo-info nil)))))
613+
614+
(ert-deftest ai-code-test-detect-todo-info-org-non-todo-headline-returns-nil ()
615+
"Test `ai-code--detect-todo-info' returns nil for non-TODO Org headlines."
616+
(with-temp-buffer
617+
(require 'org)
618+
(setq buffer-file-name "notes.org")
619+
(insert "* Regular heading\n")
620+
(org-mode)
621+
(goto-char (point-min))
622+
623+
(cl-letf (((symbol-function 'region-active-p) (lambda () nil)))
624+
(should-not (ai-code--detect-todo-info nil)))))
625+
626+
(ert-deftest ai-code-test-detect-todo-info-org-todo-colon-prefix ()
627+
"Test `ai-code--detect-todo-info' detects `TODO:' prefixed Org headlines."
628+
(with-temp-buffer
629+
(require 'org)
630+
(setq buffer-file-name "plan.org")
631+
(insert "** TODO: Implement auth module\n")
632+
(org-mode)
633+
(goto-char (point-min))
634+
635+
(cl-letf (((symbol-function 'region-active-p) (lambda () nil)))
636+
(let ((result (ai-code--detect-todo-info nil)))
637+
(should result)
638+
(should (string-match-p "TODO:" (nth 0 result)))))))
639+
640+
(ert-deftest ai-code-test-implement-todo-default-action-skips-completing-read ()
641+
"Test that passing default-action skips the completing-read prompt."
642+
(with-temp-buffer
643+
(setq buffer-file-name "test.el")
644+
(setq-local comment-start ";")
645+
(setq-local comment-end "")
646+
(insert ";; TODO: implement feature\n")
647+
(goto-char (point-min))
648+
649+
(let (captured-prompt (completing-read-called nil))
650+
(cl-letf (((symbol-function 'completing-read)
651+
(lambda (&rest _args)
652+
(setq completing-read-called t)
653+
"Code change"))
654+
((symbol-function 'ai-code-read-string)
655+
(lambda (_label input) input))
656+
((symbol-function 'ai-code--get-clipboard-text) (lambda () nil))
657+
((symbol-function 'ai-code--get-context-files-string) (lambda () ""))
658+
((symbol-function 'ai-code--format-repo-context-info) (lambda () ""))
659+
((symbol-function 'ai-code--get-function-name-for-comment) (lambda () nil))
660+
((symbol-function 'which-function) (lambda () nil))
661+
((symbol-function 'region-active-p) (lambda () nil))
662+
((symbol-function 'ai-code--insert-prompt)
663+
(lambda (p) (setq captured-prompt p))))
664+
665+
(ai-code--implement-todo--build-and-send-prompt nil "Code change")
666+
667+
(should (stringp captured-prompt))
668+
(should-not completing-read-called)
669+
(should (string-match-p "Please implement code" captured-prompt))))))
670+
671+
(ert-deftest ai-code-test-implement-todo-nil-default-action-prompts-user ()
672+
"Test that nil default-action still prompts user with completing-read."
673+
(with-temp-buffer
674+
(setq buffer-file-name "test.el")
675+
(setq-local comment-start ";")
676+
(setq-local comment-end "")
677+
(insert ";; TODO: implement feature\n")
678+
(goto-char (point-min))
679+
680+
(let (captured-prompt (completing-read-called nil))
681+
(cl-letf (((symbol-function 'completing-read)
682+
(lambda (_prompt candidates &rest _args)
683+
(setq completing-read-called t)
684+
(if (and (listp candidates)
685+
(member "Code change" candidates))
686+
"Code change"
687+
(if (listp candidates) (car candidates) ""))))
688+
((symbol-function 'ai-code-read-string)
689+
(lambda (_label input) input))
690+
((symbol-function 'ai-code--get-clipboard-text) (lambda () nil))
691+
((symbol-function 'ai-code--get-context-files-string) (lambda () ""))
692+
((symbol-function 'ai-code--format-repo-context-info) (lambda () ""))
693+
((symbol-function 'ai-code--get-function-name-for-comment) (lambda () nil))
694+
((symbol-function 'which-function) (lambda () nil))
695+
((symbol-function 'region-active-p) (lambda () nil))
696+
((symbol-function 'ai-code--insert-prompt)
697+
(lambda (p) (setq captured-prompt p))))
698+
699+
(ai-code--implement-todo--build-and-send-prompt nil nil)
700+
701+
(should (stringp captured-prompt))
702+
(should completing-read-called)))))
703+
704+
(ert-deftest ai-code-test-code-change-passes-code-change-action ()
705+
"Test that `ai-code-code-change' passes \"Code change\" as default-action."
706+
(with-temp-buffer
707+
(setq buffer-file-name "test.el")
708+
(setq-local comment-start ";")
709+
(setq-local comment-end "")
710+
(insert ";; TODO: implement feature\n")
711+
(goto-char (point-min))
712+
713+
(let (captured-default-action)
714+
(cl-letf (((symbol-function 'ai-code--get-clipboard-text) (lambda () nil))
715+
((symbol-function 'ai-code-implement-todo)
716+
(lambda (_arg &optional default-action)
717+
(setq captured-default-action default-action)))
718+
((symbol-function 'region-active-p) (lambda () nil)))
719+
720+
(ai-code-code-change nil)
721+
722+
(should (equal captured-default-action "Code change"))))))
723+
584724
(provide 'test_ai-code-change)
585725

586726
;;; test_ai-code-change.el ends here

test/test_ai-code-claude-code.el

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
((symbol-function 'ai-code-backends-infra--resolve-start-command)
2626
(lambda (&rest _args)
2727
(list :command "claude")))
28+
((symbol-function 'ai-code-mcp-agent-prepare-launch)
29+
(lambda (&rest _args) nil))
2830
((symbol-function 'ai-code-backends-infra--toggle-or-create-session)
2931
(lambda (&rest args)
3032
(cl-destructuring-bind

0 commit comments

Comments
 (0)