;;;; -*- mode:emacs-lisp; coding:utf-8; lexical-binding:t -*- ;;;;************************************************************************** ;;;;FILE: pjb-objc-ide.el ;;;;LANGUAGE: emacs lisp ;;;;SYSTEM: POSIX ;;;;USER-INTERFACE: NONE ;;;;DESCRIPTION ;;;; ;;;; Some Objective-C refactoring tools. ;;;; ;;;;AUTHORS ;;;; <PJB> Pascal Bourguignon <pbourguignon@dxo.com> ;;;;MODIFICATIONS ;;;; 2013-01-22 <PJB> Created. ;;;;BUGS ;;;;LEGAL ;;;; AGPL3 ;;;; ;;;; Copyright Pascal Bourguignon 2013 - 2013 ;;;; ;;;; This program is free software: you can redistribute it and/or modify ;;;; it under the terms of the GNU Affero General Public License as published by ;;;; the Free Software Foundation, either version 3 of the License, or ;;;; (at your option) any later version. ;;;; ;;;; This program is distributed in the hope that it will be useful, ;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;;; GNU Affero General Public License for more details. ;;;; ;;;; You should have received a copy of the GNU Affero General Public License ;;;; along with this program. If not, see <http://www.gnu.org/licenses/>. ;;;;************************************************************************** (require 'cl) (require 'cedet) (require 'pjb-strings) (require 'pjb-emacs) (require 'pjb-objc-parser) (defvar *pjb-objc-ide--nslog-function* "NSLog" "The Objective-C function to us to log messages. It should have the same signature as NSLog.") (defvar *pjb-objc-ide--entry-log-tag* "DEBUG" "A tag that will be put in comment on the lines inserted by `pjb-objc-ide--add-method-entry-log'.") (defun pjb-objc-ide--ns-class-type-p (type) ;; TODO: It would be better to collect the exact list of NS classes… (and (listp type) (eq (second type) '*) (null (cddr type)) (symbolp (first type)) (string= "NS" (substring (symbol-name (first type)) 0 2)))) (defun pjb-objc-ide--rect-formatter-arguments (argument) (mapcar (lambda (fmt) (intern (format fmt argument))) '("%s.origin.x" "%s.origin.y" "%s.size.width" "%s.size.height"))) (defun pjb-objc-ide--point-formatter-arguments (argument) (mapcar (lambda (fmt) (intern (format fmt argument))) '("%s.x" "%s.y"))) (defun pjb-objc-ide--size-formatter-arguments (argument) (mapcar (lambda (fmt) (intern (format fmt argument))) '("%s.width" "%s.height"))) (defun pjb-objc-ide--range-formatter-arguments (argument) (mapcar (lambda (fmt) (intern (format fmt argument))) '("%s.location" "%s.length"))) (defparameter *pjb-objc-ide--type-formatter-map* '(((NSUInteger) . "%d") ((NSInteger) . "%d") ((BOOL) . "%d") ((CGFloat) . "%f") ((int) . "%d") ((unsigned) . "%u") ((unsigned int) . "%u") ((char) . "%c") ((char *) . "%s") ((const char *) . "%s") ((id) . "%@") ((NSRange) . ("{l=%d,c=%d}" pjb-objc-ide--range-formatter-arguments)) ((NSPoint) . ("{x=%f,y=%f}" pjb-objc-ide--point-formatter-arguments)) ((NSSize) . ("{w=%f,h=%f}" pjb-objc-ide--size-formatter-arguments)) ((NSRect) . ("{{x=%f,y=%f},{w=%f,h=%f}}" pjb-objc-ide--rect-formatter-arguments)) (pjb-objc-ide--ns-class-type-p . "%@")) "A list of conses. The car slots are lists representing Objective-C types, or a predicate name to be called to match the type. The cdr slots are NSLog formatters: either a string to be inserted into the formatter string of NSLog, or a list containing a formatter string, and a function to build a list of arguments to NSLog for this formatter string, from the expression to be formatted. ") (defun pjb-objc-ide--formatter-for-type (type) (loop for (type-match . formatter) in *pjb-objc-ide--type-formatter-map* do (when (if (and (atom type-match) (fboundp type-match)) (funcall type-match type) (equal type-match type)) (return formatter)) finally (return (cond ((and (atom type) (char= ?* (last-char (ensure-string type)))) (pjb-objc-ide--formatter-for-type (list (intern (butlast-char (ensure-string type))) '*))) ((and (listp type) (null (rest type)) (atom (first type)) (char= ?* (last-char (ensure-string (first type))))) (pjb-objc-ide--formatter-for-type (list (intern (butlast-char (ensure-string (first type)))) '*))) (t nil))))) (assert (equal (mapcar 'pjb-objc-ide--formatter-for-type '((NSInteger) (NSUInteter) (BOOL) (char) (char *) (const char *) (id) (NSString *) (NSMutableString *) (NSDictionary *) (NSMutableArray *) (something *) (something) NSArray* (NSArray*))) '("%d" nil "%d" "%c" "%s" "%s" "%@" "%@" "%@" "%@" "%@" nil nil "%@" "%@"))) (defun pjb-objc-ide--princ-to-string (x) (let ((print-circle nil)) (format "%s" x))) (defun pjb-objc-ide--process-method (processor &optional count) "Process method implementations. If `count' is negative then processes all the implementation methods from the point down to the end of the buffer, otherwise process the given number of methods (default 1). Calls the function `processor' with as arguments, the method signature, the start point and end point of the body region (including braces), and with the current point before the opening brace (= start-pt). The excursion is saved while running `processor'. " (loop with count = (or count 1) for i from 0 while (or (minusp count) (< i count)) do (incf i) (c-forward-comments) (if (re-search-forward "\\(\\s-\\|\n\\)*[-+]\\(\\s-\\|\n\\)*(" nil t) (progn (goto-char (match-beginning 0)) (let ((signature (pjb-objc-parser--parse-method-signature))) (when (looking-at "\\(\\s-\\|\n\\)*{") (forward-sexp) (let ((end-pt (point))) (backward-sexp) (save-excursion (funcall processor signature (point) end-pt)))))) (return)))) (defun pjb-objc-ide--insert-tag () "Insert a tag comment on a newline." (insert (format " /*** %s ***/ " *pjb-objc-ide--entry-log-tag*))) (defun pjb-objc-ide--insert-method-entry-log (&optional count) "Insert a logging statement at the entry of the objective-c method dumping the parameters. If `count' is negative then processes all the implementation methods from the point down to the end of the buffer, otherwise process the given number of methods (default 1). The excursion is saved. " (interactive "p") (let ((nil-types '())) (pjb-objc-ide--process-method (lambda (signature start-pt end-pt) (down-list) (insert "\n") (pjb-objc-ide--insert-tag) (insert (format "%s(@\"%%s" *pjb-objc-ide--nslog-function*)) (let ((formatter-parameters '())) (dolist (param (cons (pjb-objc-parameter :name 'self :type '(id)) (pjb-objc-method-signature-parameters signature))) (let* ((type (pjb-objc-parameter-type param)) (formatter (pjb-objc-ide--formatter-for-type type))) (if formatter (let ((args (if (listp formatter) (funcall (second formatter) (pjb-objc-parameter-name param)) (list (pjb-objc-parameter-name param)))) (formatter (if (listp formatter) (first formatter) formatter))) (push (cons (pjb-objc-parameter-name param) args) formatter-parameters) (insert (format ", %%@ = %s" formatter))) (pushnew type nil-types :test (function equal))))) (insert "\", __PRETTY_FUNCTION__") (dolist (param (reverse formatter-parameters)) (insert (format ", @\"%s\", %s" (car param) (mapconcat (function pjb-objc-ide--princ-to-string) (cdr param) ", "))))) (insert ");")) count) (when nil-types (message "Objective-C types without a formatter (see `*pjb-objc-ide--type-formatter-map*'): %S" nil-types)))) (defun pjb-objc-ide--insert-method-return-log (&optional count) "Insert a logging statement before each return statement in the objective-c method dumping the returned result. If `count' is negative then processes all the implementation methods from the point down to the end of the buffer, otherwise process the given number of methods (default 1). The excursion is saved. " (interactive "p") (let ((nil-types '())) (pjb-objc-ide--process-method (lambda (signature start-pt end-pt) ;; We assume return statements are never left alone, but are ;; always in a block. Therefore we can insert before them as ;; many statements we want, including another return statement. ;; ;; { ;; return a+b; ;; } ;; --> ;; { ;; /*** TAG ***/ ResultTypeFromMethodSignature result=a+b; ;; /*** TAG ***/ NSLog(@"%p returns %d", __PRETTY_FUNCTION__, result); ;; /*** TAG ***/ return result; ;; return a+b; ;; } (with-marker (end-pt end-pt) (loop while (re-search-forward "^\\s-*return \\([^;]*\\);" end-pt t) do (with-marker (mend (match-end 0)) (let ((result-type (pjb-objc-method-signature-result-type signature)) (result-variable (gensym "result_")) (result-expression (match-string 1))) (goto-char (match-beginning 0)) (pjb-objc-ide--insert-tag) (insert (format "%s %s=%s;\n" (mapconcat (function pjb-objc-ide--princ-to-string) result-type " ") result-variable result-expression)) (pjb-objc-ide--insert-tag) (insert (format "%s(@\"%%s returns " *pjb-objc-ide--nslog-function*)) (let ((formatter (pjb-objc-ide--formatter-for-type result-type))) (if formatter (let ((results (if (listp formatter) (funcall (second formatter) result-variable) (list result-variable))) (formatter (if (listp formatter) (first formatter) formatter))) (insert (format "%s\", __PRETTY_FUNCTION__, %s" formatter (mapconcat (function pjb-objc-ide--princ-to-string) results ", ")))) (progn (insert (format "<%s>\", __PRETTY_FUNCTION__" result-type)) (pushnew result-type nil-types :test (function equal))))) (insert ");\n") (pjb-objc-ide--insert-tag) (insert (format "return %s;\n" result-variable))) (goto-char mend))))) count) (when nil-types (message "Objective-C types without a formatter (see `*pjb-objc-ide--type-formatter-map*'): %S" nil-types)))) (defun pjb-objc-ide-synthesize-properties () "Replace all @property <type> <identifier>; lines by @synthesize <identifier>; lines." ;; TODO: The right thing would be to search all properties in class ;; headers and superclasses headers, and to synthesize those ;; that need to be synthesized, depending of the @property ;; declaration and the presence of methods in the source ;; files... (interactive) (goto-char (point-min)) (while (re-search-forward "^[ \t]*@property\\>.*\\(\\<[_A-Za-z][_A-Za-z0-9]*\\)[ \t\n]*;" (point-max) t) (let ((name (match-string 1))) (delete-region (match-beginning 0) (match-end 0)) (insert (format "@synthesize %s;" name))))) (defun pjb-objc-ide-beginning-of-class () (interactive) (when (re-search-backward "@\\(\\(interface\\|implementation\\|prototype\\) +\\([A-Za-z0-9_]+\\)\\|end\\)" nil t) (goto-char (match-beginning 0)))) (defun pjb-objc-ide-current-class () (save-excursion (when (re-search-backward "@\\(\\(interface\\|implementation\\|prototype\\) +\\([A-Za-z0-9_]+\\)\\|end\\)" nil t) (let ((token (match-string 2)) (name (match-string 3))) (and token name))))) (defun pjb-objc-ide-current-superclass () (save-window-excursion (save-excursion (when (re-search-backward "@\\(\\(\\(interface\\|implementation\\|prototype\\) +\\([A-Za-z0-9_]+\\)\\( *: *\\([A-Za-z0-9_]+\\)\\)?\\)\\|end\\)" nil t) (let ((token (match-string 3)) (name (match-string 4)) (super (match-string 6))) (when token (or super ;; TODO: search all @interface/@implementation/@prototype ;; in the current file, in pjb-thi-other-file, and ;; in all the files in the same directory. (progn ;; Quick and dirty: (toggle-header/implementation) (goto-char (point-min)) (when (re-search-forward (format "@interface +%s *: *\\([A-Za-z0-9_]+\\)" name) nil t) (match-string 1)))))))))) (defun pjb-objc-ide-find-superclass-file () (interactive) (let ((superclass (pjb-objc-ide-current-superclass))) (when superclass (message "superclass = %s" superclass) (sources-find-file-named (format "%s.h" superclass)) (goto-char (point-min)) (re-search-forward (format "@interface +%s" superclass) nil t)))) (defun insert-braces (&optional arg) "Enclose following ARG sexps in braces. Leave point after open-paren. A negative ARG encloses the preceding ARG sexps instead. No argument is equivalent to zero: just insert `{}' and leave point between. If `parens-require-spaces' is non-nil, this command also inserts a space before and after, depending on the surrounding characters. If region is active, insert enclosing characters at region boundaries. This command assumes point is not in a string or comment." (interactive "P") (insert-pair arg ?\{ ?\})) (defun pjb-ide-beginning-of-line-to-point () (buffer-substring (save-excursion (beginning-of-line) (point)) (point))) (defun pjb-ide-tag-comment (tag) (case tag ((1) "/*** PJB-DEBUG ***/") ((-1) "//*** PJB-DISABLED ***//") (otherwise "/*** PJB ***/"))) (defun pjb-ide-insert-tag-comment (&optional tag) (interactive "p") (let ((tag (or tag 0))) (unless (string= "" (cl:string-trim " \t" (pjb-ide-beginning-of-line-to-point))) (insert "\n")) (if (region-active-p) (let ((start (region-beginning)) (end (region-end))) (message "tag=%S start=%S end=%S" tag start end) (goto-char start) (with-marker (end end) (while (< (point) end) (insert (pjb-ide-tag-comment tag)) (beginning-of-line 2)))) (insert (pjb-ide-tag-comment tag))))) (defun pjb-ide-insert-documentation-comment () (interactive) (insert " /** * * * */ ")) (provide 'pjb-objc-ide)