diff --git a/ai-code-change.el b/ai-code-change.el index 7f67ca3..17f2df9 100644 --- a/ai-code-change.el +++ b/ai-code-change.el @@ -147,9 +147,8 @@ Returns (TEXT START-POS END-POS) if TODO found, nil otherwise." (let ((heading-line (buffer-substring-no-properties (line-beginning-position) (line-end-position)))) - (when (and (or (org-get-todo-state) - (ai-code--implement-todo--org-todo-headline-p heading-line)) - (not (org-entry-is-done-p))) + (when (and (not (org-entry-is-done-p)) + (not (string-match-p "^\\*+ DONE " heading-line))) (list heading-line (line-beginning-position) (line-end-position)))))))))) @@ -285,6 +284,8 @@ Otherwise implement comments for the entire current file. Argument ARG is the prefix argument. Optional DEFAULT-ACTION skips the action prompt when non-nil." ;; 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. + ;; DONE: for this command triggered from org-mode file buffer. We want to let user choose if they want to add the condense result summary as a section (org headline), at the end of current section under cursor. If user choose yes. The prompt should let AI know this and where to add (maybe let it know current file, headline / cursor position maybe), so that it can add result summary. + ;; DONE: This result summary looks good. Is it possible to move this feature to the function just before sending prompt to AI, so that it can be applied to other code change or question asking command as well? The key point is to let user choose if they want to have this summary added to the file, and where to add, so that the prompt can include this requirement and context. (interactive "P") (if (not buffer-file-name) (user-error "Error: buffer-file-name must be available") @@ -363,12 +364,9 @@ The plist contains `:heading-line', `:content', and `:line-number'." (let* ((line-number (line-number-at-pos (point))) (heading-line (buffer-substring-no-properties (line-beginning-position) - (line-end-position))) - (todo-state (org-get-todo-state)) - (todo-prefix-p - (ai-code--implement-todo--org-todo-headline-p heading-line))) - (when (and (or todo-state todo-prefix-p) - (not (org-entry-is-done-p))) + (line-end-position)))) + (when (and (not (org-entry-is-done-p)) + (not (string-match-p "^\\*+ DONE " heading-line))) (let* ((content-start (save-excursion (forward-line 1) (point))) @@ -402,6 +400,7 @@ The plist contains `:heading-line', `:content', and `:line-number'." ARG is the prefix argument for clipboard context. Optional DEFAULT-ACTION skips the completing-read prompt when non-nil." ;; 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 + ;; DONE: currently ai-code-implement-todo work on the org-mode TODO headline. But I want it be able to work on any org-mode headline, no matter it has TODO keyword or not. Please also update the ai-code-code-change and @ai-code-discussion.el#ai-code-ask-question, for the org headline detection code (currently only detect org TODO headline) to be consistent with this function. (let* ((clipboard-context (when arg (ai-code--get-clipboard-text))) (current-line (string-trim (thing-at-point 'line t))) (current-line-number (line-number-at-pos (point))) @@ -440,7 +439,7 @@ Optional DEFAULT-ACTION skips the completing-read prompt when non-nil." (ai-code--is-comment-block region-text))) ;; Validate scenario before prompting user (_ (unless (or org-todo-section-info region-text is-comment) - (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"))) + (user-error "Current line is not a TODO comment or Org headline and cannot proceed with `ai-code-implement-todo'. Please select a TODO comment (not DONE), an Org headline (not DONE), a region of comments, or activate on a blank line"))) (_ (unless region-comment-block-p (user-error "Selected region must be a comment block"))) (action-intent (or default-action @@ -452,8 +451,8 @@ Optional DEFAULT-ACTION skips the completing-read prompt when non-nil." (cond ((and ask-question-p org-todo-section-info) (if (and clipboard-context (string-match-p "\\S-" clipboard-context)) - "Question about Org TODO headline (clipboard context): " - "Question about Org TODO headline: ")) + "Question about Org headline (clipboard context): " + "Question about Org headline: ")) (ask-question-p (if (and clipboard-context (string-match-p "\\S-" clipboard-context)) "Question about TODO comment (clipboard context): " @@ -461,7 +460,7 @@ Optional DEFAULT-ACTION skips the completing-read prompt when non-nil." ((and org-todo-section-info clipboard-context (string-match-p "\\S-" clipboard-context)) - "TODO implementation instruction for Org TODO headline (clipboard context): ") + "Implementation instruction for Org headline (clipboard context): ") ((and clipboard-context (string-match-p "\\S-" clipboard-context)) (cond @@ -469,7 +468,7 @@ Optional DEFAULT-ACTION skips the completing-read prompt when non-nil." (is-comment "TODO implementation instruction (clipboard context): ") (function-name (format "TODO implementation instruction for function %s (clipboard context): " function-name)) (t "TODO implementation instruction (clipboard context): "))) - (org-todo-section-info "TODO implementation instruction for Org TODO headline: ") + (org-todo-section-info "Implementation instruction for Org headline: ") (region-text "TODO implementation instruction: ") (is-comment "TODO implementation instruction: ") (function-name (format "TODO implementation instruction for function %s: " function-name)) @@ -477,7 +476,7 @@ Optional DEFAULT-ACTION skips the completing-read prompt when non-nil." (initial-input (cond ((and ask-question-p org-todo-section-info) - (format "Regarding this Org TODO headline on line %d:\n%s%s%s" + (format "Regarding this Org headline on line %d:\n%s%s%s" org-line-number org-section-block function-context files-context-string)) ((and ask-question-p region-text) (format "Regarding this TODO comment block in the selected region:\n%s\n%s%s%s" @@ -486,7 +485,7 @@ Optional DEFAULT-ACTION skips the completing-read prompt when non-nil." (format "Regarding this TODO comment on line %d: '%s'%s%s" current-line-number current-line function-context files-context-string)) (org-todo-section-info - (format "Please implement code for this Org TODO headline first. After implementing, keep the Org TODO headline in place and use the headline and content as prompt context.\nLine %d:\n%s%s%s" + (format "Please implement code for this Org headline first. After implementing, keep the Org headline in place and use the headline and content as prompt context.\nLine %d:\n%s%s%s" org-line-number org-section-block function-context files-context-string)) (region-text diff --git a/ai-code-prompt-mode.el b/ai-code-prompt-mode.el index 5838d15..bc4c44f 100644 --- a/ai-code-prompt-mode.el +++ b/ai-code-prompt-mode.el @@ -421,7 +421,15 @@ If PROMPT-TEXT is a command (starts with /), execute it directly instead." (if (and (string-prefix-p "/" processed-prompt) (not (string-match-p " " processed-prompt))) (ai-code--execute-command processed-prompt) - (ai-code--write-prompt-to-file-and-send processed-prompt)))) + (let* ((append-summary-p (and (derived-mode-p 'ai-code-prompt-mode) + (org-at-heading-p) + (y-or-n-p "Append result summary to current section? "))) + (final-prompt (if append-summary-p + (concat processed-prompt + (format "\n\nAfter completing, append a concise result summary as a sub-heading at the end of the current section in file %s near line %d." + buffer-file-name (line-number-at-pos))) + processed-prompt))) + (ai-code--write-prompt-to-file-and-send final-prompt))))) ;; Define the AI Prompt Mode (derived from org-mode) ;;;###autoload diff --git a/test/test_ai-code-change.el b/test/test_ai-code-change.el index f253f69..3097355 100644 --- a/test/test_ai-code-change.el +++ b/test/test_ai-code-change.el @@ -506,7 +506,7 @@ is between the function definition and its body." (should (string-match-p "[Qq]uestion" captured-label)) (should-not (string-match-p "implementation" captured-label)))))) -(ert-deftest ai-code-test-ai-code-implement-todo-org-section-includes-heading-and-content () +(ert-deftest ai-code-test-implement-todo-org-section-includes-heading-and-content () "Test Org TODO section is used as prompt context without requiring comment syntax." (with-temp-buffer (require 'org) @@ -577,7 +577,7 @@ is between the function definition and its body." (ai-code-implement-todo nil) (should (stringp captured-prompt)) - (should (string-match-p "Regarding this Org TODO headline" captured-prompt)) + (should (string-match-p "Regarding this Org headline" captured-prompt)) (should (string-match-p "TODO: what is the most important verse in Bible" captured-prompt)))))) @@ -611,17 +611,23 @@ is between the function definition and its body." (cl-letf (((symbol-function 'region-active-p) (lambda () nil))) (should-not (ai-code--detect-todo-info nil))))) -(ert-deftest ai-code-test-detect-todo-info-org-non-todo-headline-returns-nil () - "Test `ai-code--detect-todo-info' returns nil for non-TODO Org headlines." +(ert-deftest ai-code-test-detect-todo-info-org-plain-headline-detected () + "Test `ai-code--detect-todo-info' detects plain Org headlines without TODO keyword." (with-temp-buffer (require 'org) (setq buffer-file-name "notes.org") (insert "* Regular heading\n") + (insert "Some content.\n") (org-mode) (goto-char (point-min)) (cl-letf (((symbol-function 'region-active-p) (lambda () nil))) - (should-not (ai-code--detect-todo-info nil))))) + (let ((result (ai-code--detect-todo-info nil))) + (should result) + (should (stringp (nth 0 result))) + (should (string-match-p "Regular heading" (nth 0 result))) + (should (integerp (nth 1 result))) + (should (integerp (nth 2 result))))))) (ert-deftest ai-code-test-detect-todo-info-org-todo-colon-prefix () "Test `ai-code--detect-todo-info' detects `TODO:' prefixed Org headlines." @@ -721,6 +727,277 @@ is between the function definition and its body." (should (equal captured-default-action "Code change")))))) +(ert-deftest ai-code-test-get-org-section-info-plain-headline () + "Test `ai-code--implement-todo--get-org-todo-section-info' returns info for plain Org headline." + (with-temp-buffer + (require 'org) + (setq buffer-file-name "notes.org") + (insert "* Regular heading\n") + (insert "Some content.\n") + (org-mode) + (goto-char (point-min)) + + (let ((result (ai-code--implement-todo--get-org-todo-section-info))) + (should result) + (should (string-match-p "Regular heading" + (plist-get result :heading-line))) + (should (string= "Some content." (plist-get result :content)))))) + +(ert-deftest ai-code-test-implement-todo-org-plain-headline-works () + "Test `ai-code-implement-todo' works on a plain Org headline." + (with-temp-buffer + (require 'org) + (setq buffer-file-name "notes.org") + (insert "* Implement search feature\n") + (insert "Use fuzzy matching.\n") + (org-mode) + (goto-char (point-min)) + + (let (captured-prompt) + (cl-letf (((symbol-function 'completing-read) + (lambda (&rest _) "Code change")) + ((symbol-function 'ai-code-read-string) + (lambda (_label input) input)) + ((symbol-function 'ai-code--get-clipboard-text) (lambda () nil)) + ((symbol-function 'ai-code--get-context-files-string) (lambda () "")) + ((symbol-function 'ai-code--format-repo-context-info) (lambda () "")) + ((symbol-function 'which-function) (lambda () nil)) + ((symbol-function 'region-active-p) (lambda () nil)) + ((symbol-function 'ai-code--insert-prompt) + (lambda (prompt) (setq captured-prompt prompt)))) + + (ai-code-implement-todo nil) + + (should (stringp captured-prompt)) + (should (string-match-p "Implement search feature" captured-prompt)) + (should (string-match-p "Use fuzzy matching" captured-prompt)))))) + +(ert-deftest ai-code-test-code-change-routes-to-implement-todo-on-plain-org-headline () + "Test `ai-code-code-change' routes to `ai-code-implement-todo' on plain Org headline." + (with-temp-buffer + (require 'org) + (setq buffer-file-name "notes.org") + (insert "* Regular heading\n") + (org-mode) + (goto-char (point-min)) + + (let (captured-default-action) + (cl-letf (((symbol-function 'ai-code--get-clipboard-text) (lambda () nil)) + ((symbol-function 'ai-code-implement-todo) + (lambda (_arg &optional default-action) + (setq captured-default-action default-action))) + ((symbol-function 'region-active-p) (lambda () nil))) + + (ai-code-code-change nil) + + (should (equal captured-default-action "Code change")))))) + +(ert-deftest ai-code-test-implement-todo-org-no-append-summary-in-build () + "Test that `build-and-send-prompt' no longer asks about appending summary (moved to insert-prompt)." + (with-temp-buffer + (require 'org) + (setq buffer-file-name "todo.org") + (insert "* TODO Build feature\n") + (insert "Details here.\n") + (org-mode) + (goto-char (point-min)) + + (let (y-or-n-called) + (cl-letf (((symbol-function 'completing-read) + (lambda (&rest _) "Code change")) + ((symbol-function 'y-or-n-p) + (lambda (_prompt) + (setq y-or-n-called t) + nil)) + ((symbol-function 'ai-code-read-string) + (lambda (_label input) input)) + ((symbol-function 'ai-code--get-clipboard-text) (lambda () nil)) + ((symbol-function 'ai-code--get-context-files-string) (lambda () "")) + ((symbol-function 'ai-code--format-repo-context-info) (lambda () "")) + ((symbol-function 'which-function) (lambda () nil)) + ((symbol-function 'region-active-p) (lambda () nil)) + ((symbol-function 'ai-code--insert-prompt) (lambda (_p) nil))) + + (ai-code--implement-todo--build-and-send-prompt nil) + + (should-not y-or-n-called))))) + +(ert-deftest ai-code-test-implement-todo-comment-no-append-summary-asked () + "Test that `y-or-n-p' is NOT asked on regular TODO comment path." + (with-temp-buffer + (setq buffer-file-name "test.el") + (setq-local comment-start ";") + (setq-local comment-end "") + (insert ";; TODO: implement feature\n") + (goto-char (point-min)) + + (let (y-or-n-called) + (cl-letf (((symbol-function 'completing-read) + (lambda (&rest _) "Code change")) + ((symbol-function 'y-or-n-p) + (lambda (_prompt) + (setq y-or-n-called t) + nil)) + ((symbol-function 'ai-code-read-string) + (lambda (_label input) input)) + ((symbol-function 'ai-code--get-clipboard-text) (lambda () nil)) + ((symbol-function 'ai-code--get-context-files-string) (lambda () "")) + ((symbol-function 'ai-code--format-repo-context-info) (lambda () "")) + ((symbol-function 'ai-code--get-function-name-for-comment) (lambda () nil)) + ((symbol-function 'which-function) (lambda () nil)) + ((symbol-function 'region-active-p) (lambda () nil)) + ((symbol-function 'ai-code--insert-prompt) (lambda (_p) nil))) + + (ai-code--implement-todo--build-and-send-prompt nil) + + (should-not y-or-n-called))))) + +(ert-deftest ai-code-test-implement-todo-org-build-prompt-no-summary-text () + "Test that `build-and-send-prompt' prompt does NOT contain summary (moved to insert-prompt)." + (with-temp-buffer + (require 'org) + (setq buffer-file-name "/tmp/project/todo.org") + (insert "* TODO Build feature\n") + (insert "Details here.\n") + (org-mode) + (goto-char (point-min)) + + (let (captured-prompt) + (cl-letf (((symbol-function 'completing-read) + (lambda (&rest _) "Code change")) + ((symbol-function 'ai-code-read-string) + (lambda (_label input) input)) + ((symbol-function 'ai-code--get-clipboard-text) (lambda () nil)) + ((symbol-function 'ai-code--get-context-files-string) (lambda () "")) + ((symbol-function 'ai-code--format-repo-context-info) (lambda () "")) + ((symbol-function 'which-function) (lambda () nil)) + ((symbol-function 'region-active-p) (lambda () nil)) + ((symbol-function 'ai-code--insert-prompt) + (lambda (p) (setq captured-prompt p)))) + + (ai-code--implement-todo--build-and-send-prompt nil) + + (should (stringp captured-prompt)) + (should-not (string-match-p "summary" captured-prompt)))))) + +(ert-deftest ai-code-test-implement-todo-org-append-summary-no () + "Test that choosing no does NOT add summary instruction to prompt." + (with-temp-buffer + (require 'org) + (setq buffer-file-name "/tmp/project/todo.org") + (insert "* TODO Build feature\n") + (insert "Details here.\n") + (org-mode) + (goto-char (point-min)) + + (let (captured-prompt) + (cl-letf (((symbol-function 'completing-read) + (lambda (&rest _) "Code change")) + ((symbol-function 'ai-code-read-string) + (lambda (_label input) input)) + ((symbol-function 'ai-code--get-clipboard-text) (lambda () nil)) + ((symbol-function 'ai-code--get-context-files-string) (lambda () "")) + ((symbol-function 'ai-code--format-repo-context-info) (lambda () "")) + ((symbol-function 'which-function) (lambda () nil)) + ((symbol-function 'region-active-p) (lambda () nil)) + ((symbol-function 'ai-code--insert-prompt) + (lambda (p) (setq captured-prompt p)))) + + (ai-code--implement-todo--build-and-send-prompt nil) + + (should (stringp captured-prompt)) + (should-not (string-match-p "summary" captured-prompt)))))) + +(ert-deftest ai-code-test-insert-prompt-org-heading-append-summary-yes () + "Test that `ai-code--insert-prompt' appends summary instruction on prompt-mode heading when user confirms." + (with-temp-buffer + (setq buffer-file-name "/tmp/project/.ai.code.files/.ai.code.prompt.org") + (insert "* TODO Build search feature\n") + (insert "Design the API first.\n") + (ai-code-prompt-mode) + (goto-char (point-min)) + + (let (captured-prompt + (ai-code-prompt-preprocess-filepaths nil)) + (cl-letf (((symbol-function 'y-or-n-p) (lambda (_) t)) + ((symbol-function 'ai-code--write-prompt-to-file-and-send) + (lambda (prompt) (setq captured-prompt prompt)))) + + (ai-code--insert-prompt "Test prompt text") + + (should (stringp captured-prompt)) + (should (string-match-p "Test prompt text" captured-prompt)) + (should (string-match-p "summary" captured-prompt)) + (should (string-match-p "\\.ai\\.code\\.prompt\\.org" captured-prompt)))))) + +(ert-deftest ai-code-test-insert-prompt-org-heading-append-summary-no () + "Test that `ai-code--insert-prompt' does NOT append summary when user declines." + (with-temp-buffer + (setq buffer-file-name "/tmp/project/.ai.code.files/.ai.code.prompt.org") + (insert "* TODO Build search feature\n") + (insert "Design the API first.\n") + (ai-code-prompt-mode) + (goto-char (point-min)) + + (let (captured-prompt + (ai-code-prompt-preprocess-filepaths nil)) + (cl-letf (((symbol-function 'y-or-n-p) (lambda (_) nil)) + ((symbol-function 'ai-code--write-prompt-to-file-and-send) + (lambda (prompt) (setq captured-prompt prompt)))) + + (ai-code--insert-prompt "Test prompt text") + + (should (stringp captured-prompt)) + (should (string-match-p "Test prompt text" captured-prompt)) + (should-not (string-match-p "summary" captured-prompt)))))) + +(ert-deftest ai-code-test-insert-prompt-non-prompt-mode-no-summary-asked () + "Test that `ai-code--insert-prompt' does NOT ask about summary in non-prompt-mode buffer." + (with-temp-buffer + (setq buffer-file-name "/tmp/project/test.el") + (setq-local comment-start ";") + (insert ";; some code\n") + (goto-char (point-min)) + + (let (y-or-n-called captured-prompt + (ai-code-prompt-preprocess-filepaths nil)) + (cl-letf (((symbol-function 'y-or-n-p) + (lambda (_) + (setq y-or-n-called t) + nil)) + ((symbol-function 'ai-code--write-prompt-to-file-and-send) + (lambda (prompt) (setq captured-prompt prompt)))) + + (ai-code--insert-prompt "Test prompt text") + + (should-not y-or-n-called) + (should (string-match-p "Test prompt text" captured-prompt)) + (should-not (string-match-p "summary" captured-prompt)))))) + +(ert-deftest ai-code-test-insert-prompt-slash-command-on-heading-executes () + "Test that slash commands execute directly even when on a prompt-mode heading." + (with-temp-buffer + (setq buffer-file-name "/tmp/project/.ai.code.files/.ai.code.prompt.org") + (insert "* TODO Build search feature\n") + (ai-code-prompt-mode) + (goto-char (point-min)) + + (let (y-or-n-called command-executed + (ai-code-prompt-preprocess-filepaths nil)) + (cl-letf (((symbol-function 'y-or-n-p) + (lambda (_) + (setq y-or-n-called t) + t)) + ((symbol-function 'ai-code--execute-command) + (lambda (_cmd) (setq command-executed t))) + ((symbol-function 'ai-code--write-prompt-to-file-and-send) + (lambda (_) (error "Should not reach write-prompt")))) + + (ai-code--insert-prompt "/status") + + (should command-executed) + (should-not y-or-n-called))))) + (provide 'test_ai-code-change) ;;; test_ai-code-change.el ends here diff --git a/test/test_ai-code-discussion.el b/test/test_ai-code-discussion.el index a2761ab..99a7265 100644 --- a/test/test_ai-code-discussion.el +++ b/test/test_ai-code-discussion.el @@ -118,6 +118,28 @@ (should (equal captured-default-action "Ask question")))))) +(ert-deftest ai-code-test-ask-question-routes-to-implement-todo-on-plain-org-headline () + "Test `ai-code-ask-question' routes to `ai-code-implement-todo' on plain Org headline." + (with-temp-buffer + (require 'org) + (setq buffer-file-name "notes.org") + (insert "* Regular heading\n") + (org-mode) + (goto-char (point-min)) + + (let (implement-todo-called) + (cl-letf (((symbol-function 'ai-code--get-clipboard-text) (lambda () nil)) + ((symbol-function 'ai-code-implement-todo) + (lambda (_arg &optional _default-action) + (setq implement-todo-called t))) + ((symbol-function 'ai-code--ask-question-file) + (lambda (_ctx) (error "Should not reach ask-question-file"))) + ((symbol-function 'region-active-p) (lambda () nil))) + + (ai-code-ask-question nil) + + (should implement-todo-called))))) + (provide 'test_ai-code-discussion) ;;; test_ai-code-discussion.el ends here