diff --git a/company-emacs-eclim.el b/company-emacs-eclim.el index 27c9d9b..0cf2fb2 100644 --- a/company-emacs-eclim.el +++ b/company-emacs-eclim.el @@ -80,7 +80,11 @@ (mapcar (lambda (candidate) (annotate (without-redundant-prefix candidate))) - (eclim--completion-candidates))))) + ;; Company says backend is responsible for filtering prefix case. + (if company-emacs-eclim-ignore-case + (eclim--completion-candidates) + (remove-if-not #'(lambda(str) (string-prefix-p prefix str)) + (eclim--completion-candidates))))))) (defun company-emacs-eclim--annotation (candidate) (let ((str (get-text-property 0 'eclim-meta candidate))) diff --git a/eclim-completion.el b/eclim-completion.el index 7e7f65e..060caf1 100644 --- a/eclim-completion.el +++ b/eclim-completion.el @@ -161,6 +161,15 @@ buffer." (case major-mode ((java-mode javascript-mode js-mode ruby-mode groovy-mode php-mode c-mode c++-mode scala-mode) (progn + ;; Allow completion after open bracket. Eclipse/eclim do. + (when (or (eq ?\( (char-before)) + ;; Template? Technically it could be a less-than sign + ;; but it's unlikely the user completes there and + ;; no particular harm done. + (and (eq ?\< (char-before)) + (memq major-mode + '(java-mode c++-mode goovy-mode)))) + (backward-char 1)) (ignore-errors (beginning-of-thing 'symbol)) ;; Completion candidates for annotations don't include '@'. (when (eq ?@ (char-after)) diff --git a/eclim-java.el b/eclim-java.el index 1ddd205..17098e7 100644 --- a/eclim-java.el +++ b/eclim-java.el @@ -251,7 +251,7 @@ has been found." (n (read-string (concat "Rename " (cdr i) " to: ") (cdr i)))) (eclim/with-results res ("java_refactor_rename" "-p" "-e" "-f" ("-n" n) ("-o" (car i)) ("-l" (length (cdr i)))) - (if (stringp res) (error res)) + (if (stringp res) (error "%s" res)) (loop for (from to) in (mapcar (lambda (x) (list (assoc-default 'from x) (assoc-default 'to x))) res) do (when (and from to) (kill-buffer (find-buffer-visiting from)) @@ -402,10 +402,6 @@ matters for buffers containing non-ASCII characters)." (cons (if position (point) (eclim--byte-offset)) (buffer-substring-no-properties start end)))))) -(defun eclim--java-package-components (package) - "Returns the components of a Java package statement." - (split-string package "\\.")) - (defun eclim--java-current-package () "Returns the package for the class in the current buffer." (save-excursion @@ -470,57 +466,135 @@ sorts import statements. " (eclim-java-import-organize (mapcar (lambda (imports) (eclim--completing-read "Import: " (append imports '()))) res))))))) -(defun format-type (type) - (cond ((null type) nil) - ((listp (first type)) - (append (list "<") (rest (mapcan (lambda (type) (append (list ", ") (format-type type))) (first type))) (list ">") - (format-type (rest type)))) - (t (cons (let ((type-name (symbol-name (first type)))) - (when (string-match "\\(.*\\.\\)?\\(.*\\)" type-name) - (match-string 2 type-name))) - (format-type (rest type)))))) + +(defun eclim--signature-has-keyword (sig java-keyword) + "Returns true if a method signature SIG has the keyword JAVA-KEYWORD." + ;; \_< is beginning of identifier E.g. don't match do_abstract". + (string-match-p (format "\\_<%s\\_>" java-keyword) sig)) + + +(defun eclim--colorize-signature (sig) + "Minimal colorization for a method signature that we offer for completion, +so the essential bits stand out from the block of text that ido presents. +Keep this minimal: more highlighting could easily make things worse not better." + (save-match-data + (mapc #'(lambda(re-g-f) ;; expecting single match per RE + (when (string-match (elt re-g-f 0) sig) + (setq sig (replace-match + (propertize (match-string (elt re-g-f 1) sig) + 'face (elt re-g-f 2)) + nil nil sig (elt re-g-f 1))))) + '(("\\_<\\(class\\|interface\\)\\s +\\([[:alnum:]_]+\\_>\\)" + 2 font-lock-type-face) + ("\\_<\\([[:alnum:]_]+\\)(" 1 font-lock-function-name-face) + ("all [[:digit:]]+ \\w+ methods" 0 font-lock-function-name-face)))) + sig) + (defun eclim-java-implement (&optional name) - "Lets the user select from a list of methods to -implemnt/override, then inserts a skeleton for the chosen -method." + "Implement or override methods from parents of the class, prompting the +user to select with a completing read (even if one, as confirmation). If +NAME was specified programmatically, filters for that name (strict, +although only on method name not arguments) and if only one choice +implement it without prompting. The actual change is done by Eclipse +and will be close to point although not necessarily at it (e.g. if in a +sub block)." (interactive) - (eclim/with-results response ("java_impl" "-p" "-f" "-o") - (cl-flet ((join (glue items) - (cond ((null items) "") - ((= 1 (length items)) (format "%s" (first items))) - (t (reduce (lambda (a b) (format "%s%s%s" a glue b)) items)))) - (format-type (type) - (cond ((null type) nil) - ((listp (first type)) - (append (list "<") (rest (mapcan (lambda (type) (append (list ", ") (format-type type))) (first type))) (list ">") - (format-type (rest type)))) - (t (cons (let ((type-name (symbol-name (first type)))) - (when (string-match "\\(.*\\.\\)?\\(.*\\)" type-name) - (let ((package (match-string 1 type-name)) - (class (match-string 2 type-name))) - (eclim-java-import (concat package class)) - class))) - (format-type (rest type))))))) - (let* ((methods (remove-if-not (lambda (m) (or (null name) - (string-match name m))) - (mapcar (lambda (x) (replace-regexp-in-string "[ \n\t]+" " " x)) - (apply 'append - (mapcar (lambda (x) (append (assoc-default 'methods x) nil)) - (assoc-default 'superTypes response)))))) - (method (if (= 1 (length methods)) (first methods) - (eclim--completing-read "Signature: " methods))) - (sig (eclim--java-parse-method-signature method)) - (ret (assoc-default :return sig))) - (yas/expand-snippet (format "@Override\n%s %s(%s) {$0}" - (apply #'concat - (join " " (remove-if-not (lambda (m) (find m '(public protected private void))) (subseq ret 0 (1- (length ret))))) - " " - (format-type (remove-if (lambda (m) (find m '(abstract public protected private ))) ret))) - (assoc-default :name sig) - (join ", " (loop for arg in (remove-if #'null (assoc-default :arglist sig)) - for i from 0 - collect (format "%s ${arg%s}" (apply #'concat (format-type (assoc-default :type arg))) i))))))))) + (eclim/with-results list-response ("java_impl" "-p" "-f" "-o") + (let* ((supertypes (assoc-default 'superTypes list-response)) + ;; "Choices" are lists of user-friendly method names. We want to + ;; present interfaces/abstract first, otherwise Object can barge in. + (choices nil) (choices-opt nil) (choices-last nil) + ;; Maps a choice to a (supertype method1 method2...), needed + ;; when we request eclim to implement that method. + (choice-data (make-hash-table :test 'equal))) + (loop + for super-entry across supertypes do + (let* ((package (assoc-default 'packageName super-entry)) + (super-sig (assoc-default 'signature super-entry)) + ;; Erase type arguments. This looks like "class List". + (friendly-super (replace-regexp-in-string "<[^<]*>" "" super-sig)) + (full-super (concat package "." + (replace-regexp-in-string "^\\w+ " "" + friendly-super))) + (is-interface (eclim--signature-has-keyword + super-sig "interface")) + (methods (assoc-default 'methods super-entry)) + (required-methods nil)) ;; Eclim names here + (loop + for method across methods + ;; Skip if specified name doesn't match. + if (or (null name) + (string-match-p (format "\\_<%s(" (regexp-quote name)) method)) + do + ;; This regexp stuff is how vim (and thus eclim) does it. Nothing + ;; fancy. If it breaks, Google eclim/java/impl.vim for changes. + (let ((name-for-eclim + ;; Remove keywords and return type. \_< begins identifier. + (replace-regexp-in-string "^\\s *[^(]*\\(\\_<[[:alnum:]_]+(\\)" + "\\1" + ;; Remove any and all type parameters. + (replace-regexp-in-string "<[^<]*>" "" method))) + ;; For the user, we have very different requirements. I like + ;; knowing public and abstract, and the return type. I hate + ;; packages -- I'm already implementing this class so I know. + (friendly-name + ;; Packages are non-trivial to find (think Map.Entry) but + ;; if we stop at the first capitalized portion we're okay. + (replace-regexp-in-string "\\_<[[:lower:]][[:alnum:]_]+\\." + "" method)) + (is-required (or is-interface (eclim--signature-has-keyword + method "abstract")))) + (let ((choice (format "%s [%s]" friendly-name friendly-super)) + (data (list full-super name-for-eclim))) + ;; This is probably overkill but what if our package erasing + ;; resulted in duplicates? Use full name then. As in, really full. + (when (gethash choice choice-data) + (setq choice (format "%s [%s]" name-for-eclim full-super))) + (cond (is-required (push choice choices)) + ((member full-super '("java.lang.Object")) ; others like it? + (push choice choices-last)) + (t (push choice choices-opt))) + (puthash choice (list full-super name-for-eclim) choice-data) + (when is-required (push name-for-eclim required-methods))))) + ;; Since we don't allow multiple selection like Eclipse / vim, let's + ;; provide for the cases that matter. Note that full non-abstract + ;; overrides are typically a use case for *delegates*. + (when (> (length required-methods) 1) ;; 1 method already there + (let ((choice + (format "" + (length required-methods) + (cond (is-interface "missing") (name) (t "abstract")) + friendly-super)) + (data (cons full-super (reverse required-methods)))) + (push choice choices) ;; I'll not worry about conflict here. + (puthash choice data choice-data))))) + ;; Keep inital order, except for our tweaks. + (setq choices (append (nreverse choices) (reverse choices-opt) + (reverse choices-last))) + (unless choices + (if name (error "No such unimplemented method: %s" name) ;most likely + (error "No candidates to implement"))) ;; Rare, given Object ancestor. + + ;; Ask user even if only one choice, for confirmation. Otherwise it's + ;; possible to not even notice the change from a bad key combo. Unless + ;; we were called programmatically a for specific method. + (let ((choice + (if (and name (eq 1 (length choices))) + (first choices) + (funcall eclim-interactive-completion-function + "Implement: " + (mapcar #'eclim--colorize-signature choices) + nil t)))) ; require match + (setq choice (substring-no-properties choice)) ; uncolorize + (let* ((eclim-data (gethash choice choice-data)) + (super (car eclim-data)) (methods (cdr eclim-data)) + (methods-str (json-encode methods))) + (eclim/with-results impl-result ("java_impl" "-p" "-f" "-o" + ("-s" super) ("-m" methods-str)) + ;; eclim should give us a smaller list if it did something. But + ;; it's probably not worth an error in case this changes. + (revert-buffer t t t))))))) (defun eclim-package-and-class () (let ((package-name (eclim--java-current-package)) @@ -595,7 +669,10 @@ much faster than running mvn test -Dtest=TestClass#method." "-f" ("-l" line) ("-o" offset) - ("-a" choice))) + ("-a" choice)) + ;; Problem updates can be distracting, but here the user was + ;; actively trying to fix one. + (eclim--problems-update-maybe)) (message "No automatic corrections found. Sorry"))))) (defun eclim-java-show-documentation-for-current-element () @@ -649,13 +726,14 @@ much faster than running mvn test -Dtest=TestClass#method." (replace-match text) (make-text-button (match-beginning 0) (+ (match-beginning 0) (length text)) + 'follow-link t 'action 'eclim-java-show-documentation-follow-link 'url href)))) (when add-to-history (goto-char (point-max)) (insert "\n\n") - (insert-text-button "back" 'action 'eclim--java-show-documentation-go-back)) + (insert-text-button "back" 'follow-link t 'action 'eclim--java-show-documentation-go-back)) (goto-char (point-min))) diff --git a/eclim-problems.el b/eclim-problems.el index b25124a..c31e63c 100644 --- a/eclim-problems.el +++ b/eclim-problems.el @@ -33,6 +33,17 @@ :type '(choice (const :tag "Off" nil) (const :tag "On" t))) +(defcustom eclim-problems-suppress-highlights nil + "When set, error and warning highlights are disabled in source files, +although counts are printed and they remain navigable. This is +designed to be made buffer-local (by user, not eclim) most of the +time, but it also works globally." + :group 'eclim-problems + :type '(choice (const :tag "Allow" nil) + (const :tag "Suppress" t) + (sexp :tag "Suppress when" + :value (lambda() 'for-example buffer-read-only)))) + (defface eclim-problems-highlight-error-face '((t (:underline "red"))) "Face used for highlighting errors in code" @@ -68,6 +79,7 @@ (define-key eclim-mode-map (kbd "C-c C-e o") 'eclim-problems-open) (defvar eclim--problems-list nil) +(defvar eclim--problems-refreshing nil) ;; Set to true while refreshing probs. (defvar eclim--problems-filter nil) ;; nil -> all problems, w -> warnings, e -> errors (defvar eclim--problems-filefilter nil) ;; should filter by file name @@ -151,17 +163,27 @@ (overlay-put highlight 'category 'eclim-problem) (overlay-put highlight 'kbd-help (assoc-default 'message problem)))))) -(defun eclim--problems-clear-highlights () + +(defun eclim-problems-clear-highlights () + "Clears all eclim problem highlights in the current buffer. This is temporary +until the next refresh." + (interactive) (remove-overlays nil nil 'category 'eclim-problem)) + (defun eclim-problems-highlight () + "Inserts the currently active problem highlights in the current buffer, +if `eclim-problems-suppress-highlights' allows it." (interactive) (when (eclim--accepted-p (buffer-file-name)) (save-restriction (widen) - (eclim--problems-clear-highlights) - (loop for problem across (remove-if-not (lambda (p) (string= (assoc-default 'filename p) (buffer-file-name))) eclim--problems-list) - do (eclim--problems-insert-highlight problem))))) + (eclim-problems-clear-highlights) + (unless (if (functionp eclim-problems-suppress-highlights) + (funcall eclim-problems-suppress-highlights) + eclim-problems-suppress-highlights) + (loop for problem across (remove-if-not (lambda (p) (string= (assoc-default 'filename p) (buffer-file-name))) eclim--problems-list) + do (eclim--problems-insert-highlight problem)))))) (defadvice find-file (after eclim-problems-highlight-on-find-file activate) (eclim-problems-highlight)) @@ -201,10 +223,19 @@ (eclim--problem-goto-pos p))) (defun eclim-problems-correct () + "Pops up a suggestion for the current correction. This can be +invoked in either the problems buffer or a source code buffer." (interactive) (let ((p (eclim--problems-get-current-problem))) - (if (not (string-match "\\.\\(groovy\\|java\\)$" (cdr (assoc 'filename p)))) - (error "Not a Java or Groovy file. Corrections are currently supported only for Java or Groovy") + (unless (string-match "\\.\\(groovy\\|java\\)$" (cdr (assoc 'filename p))) + (error "Not a Java or Groovy file. Corrections are currently supported only for Java or Groovy")) + (if (eq major-mode 'eclim-problems-mode) + (let ((p-buffer (find-file-other-window (assoc-default 'filename p)))) + (with-selected-window (get-buffer-window p-buffer t) + ;; Intentionally DON'T save excursion. Often times we need edits. + (eclim--problem-goto-pos p) + (eclim-java-correct (cdr (assoc 'line p)) (eclim--byte-offset)))) + ;; source code buffer (eclim-java-correct (cdr (assoc 'line p)) (eclim--byte-offset))))) (defmacro eclim--with-problems-list (problems &rest body) @@ -213,14 +244,14 @@ it asynchronously." (let ((res (gensym))) `(when eclim--problems-project - (when (not (minibuffer-window-active-p (minibuffer-window))) - (message "refreshing... %s " (current-buffer))) + (setq eclim--problems-refreshing t) (eclim/with-results-async ,res ("problems" ("-p" eclim--problems-project) (when (string= "e" eclim--problems-filter) '("-e" "true"))) (loop for problem across ,res do (let ((filecell (assq 'filename problem))) (when filecell (setcdr filecell (file-truename (cdr filecell)))))) (setq eclim--problems-list ,res) (let ((,problems ,res)) + (setq eclim--problems-refreshing nil) ,@body))))) (defun eclim-problems-buffer-refresh () @@ -439,28 +470,58 @@ is convenient as it lets the user navigate between errors using `next-error' (\\[next-error])." (interactive) (lexical-let ((filecol-size (eclim--problems-filecol-size)) - (project-directory (concat (eclim--project-dir buffer-file-name) "/")) - (compil-buffer (get-buffer-create eclim--problems-compilation-buffer-name))) + (project-directory (concat (eclim--project-dir) "/")) + (compil-buffer (get-buffer-create eclim--problems-compilation-buffer-name)) + (project-name (eclim-project-name))) ; To store it in buffer. + + (with-current-buffer compil-buffer + (setq default-directory project-directory) + (setq mode-line-process + (concat ": " (propertize "refreshing" + 'face 'compilation-mode-line-run)))) + ;; Remember that the part below is asynchronous. This can be tricky. (eclim--with-problems-list problems - (with-current-buffer compil-buffer - (setq default-directory project-directory) - (setq buffer-read-only nil) - (erase-buffer) - (insert (concat "-*- mode: compilation; default-directory: " - project-directory - " -*-\n\n")) - (let ((errors 0) (warnings 0)) - (loop for problem across (eclim--problems-filtered) - do (eclim--insert-problem-compilation problem filecol-size project-directory) - (cond ((assoc-default 'warning problem) - (setq warnings (1+ warnings))) - (t - (setq errors (1+ errors))))) - (insert (format "\nCompilation results: %d errors and %d warnings." - errors warnings))) - (compilation-mode)) - (display-buffer compil-buffer 'other-window)))) - + (let (saved-user-pos) + (with-current-buffer compil-buffer + (buffer-disable-undo) + (setq buffer-read-only nil) + (setq saved-user-pos (point)) + (erase-buffer) + (let ((errors 0) (warnings 0)) + (loop for problem across (eclim--problems-filtered) do + (eclim--insert-problem-compilation + problem filecol-size project-directory) + (if (eq t (assoc-default 'warning problem)) ; :json-false, WTH + (setq warnings (1+ warnings)) + (setq errors (1+ errors)))) + (let ((msg (format + "Compilation results: %d errors, %d warnings [%s].\n" + errors warnings (current-time-string)))) + (insert "\n" msg) + (goto-char (point-min)) + (insert msg "\n")) + (compilation-mode) + ;; The above killed local variables, so recover our lexical-lets + (setq default-directory project-directory) + (setq eclim-project-name project-name) + ;; Remap the very dangerous "g" command :) A make -k in some of + ;; my projects would throw Eclipse off-balance by cleaning .classes. + ;; May look funky, but it's safe. + (local-set-key "g" 'eclim-problems-compilation-buffer) + + (setq mode-line-process + (concat ": " + (propertize (format "%d/%d" errors warnings) + 'face (when (> errors 0) + 'compilation-mode-line-fail)))))) + ;; Sometimes, buffer was already current. Note outside with-current-buf. + (unless (eq compil-buffer (current-buffer)) + (display-buffer compil-buffer 'other-window)) + (with-selected-window (get-buffer-window compil-buffer t) + (when (< saved-user-pos (point-max)) + (goto-char saved-user-pos))))))) + + (defun eclim--insert-problem-compilation (problem filecol-size project-directory) (let ((filename (first (split-string (assoc-default 'filename problem) project-directory t))) (description (assoc-default 'message problem)) @@ -477,11 +538,46 @@ is convenient as it lets the user navigate between errors using (length (eclim--filter-problems "w" t (buffer-file-name (current-buffer)) eclim--problems-list))) +(defun eclim-problems-next-same-file (&optional up) + "Moves to the next problem in the current file, with wraparound. If UP +or prefix arg, moves to previous instead; see `eclim-problems-prev-same-file'." + (interactive "P") + ;; This seems pretty inefficient, but it's fast enough. Would be even + ;; more inefficient if we didn't assume problems were sorted. + (let ((problems-file + (eclim--filter-problems nil t (buffer-file-name (current-buffer)) + eclim--problems-list)) + (pass-line (line-number-at-pos)) + (pass-col (+ (current-column) (if up 0 1))) + (first-passed nil) (last-not-passed nil)) + (when (= 0 (length problems-file)) (error "No problems in this file")) + (loop for p across problems-file until first-passed do + (let ((line (assoc-default 'line p)) + (col (assoc-default 'column p))) + (if (or (> line pass-line) + (and (= line pass-line) (> col pass-col))) + (setq first-passed p) + (setq last-not-passed p)))) + (eclim--problem-goto-pos + (or + (if up last-not-passed first-passed) + (when up (message "Moved past first error, continuing to last") + (elt problems-file (- (length problems-file) 1))) ; Ugh, vector + (progn (message "Moved past last error, continuing to first") + (elt problems-file 0)))))) + +(defun eclim-problems-prev-same-file () + "Moves to the previous problem in the same file, with wraparound." + (interactive) + (eclim-problems-next-same-file t)) + + (defun eclim-problems-modeline-string () "Returns modeline string with additional info about problems for current file" - (concat (format " : %s/%s" + (concat (format ": %s/%s" (eclim--count-current-errors) - (eclim--count-current-warnings)))) + (eclim--count-current-warnings)) + (when eclim--problems-refreshing "*"))) (provide 'eclim-problems) diff --git a/eclim.el b/eclim.el index 76ea3a5..20075c3 100644 --- a/eclim.el +++ b/eclim.el @@ -435,6 +435,26 @@ FILENAME is given, return that file's project name instead." hits)) t))) +(defun eclim-find-file-path-strict (filename &optional project directory) + "Locates a file (basename) in Eclipse. If PROJECT is a string, +searches only that project; if nil, the project of the current +file. If t, searches all Eclipse projects. If DIRECTORY is +specified, returns only files that are under that +directory. Returns a list of matching absolute paths; possibly +empty. This can be used to help resolve exception stack traces, +for example." + (let* ((results (apply #'eclim--call-process "locate_file" + "-p" (regexp-quote filename) + (if (eq project t) + (list "-s" "workspace") + (list "-s" "project" "-n" + (or project (eclim-project-name)))))) + (paths (mapcar #'(lambda(hit) (assoc-default 'path hit)) results))) + (if directory + (remove-if-not #'(lambda (f) (file-in-directory-p f directory)) paths) + paths))) + + ;;;###autoload (defun eclim/workspace-dir () (eclim--call-process "workspace_dir")) @@ -474,7 +494,7 @@ FILENAME is given, return that file's project name instead." "List of regular expressions that are matched against filenames to decide if eclim should be automatically started on a particular file. By default all files part of a project managed -by eclim can be accepted (see `eclim--accepted-filename' for more +by eclim can be accepted (see `eclim--accepted-filename-p' for more information). It is nevertheless possible to restrict eclim to some files by changing this variable. For example, a value of (\"\\\\.java\\\\'\" \"build\\\\.xml\\\\'\") can be used to restrict @@ -544,6 +564,6 @@ the use of eclim to java and ant files." (defun eclim-modeline-string () (when eclim-mode - (concat " Eclim " (eclim-problems-modeline-string)))) + (concat " Eclim" (eclim-problems-modeline-string)))) (provide 'eclim)