;;; xesam.el --- Xesam interface to search engines.
-;; Copyright (C) 2008 Free Software Foundation, Inc.
+;; Copyright (C) 2008, 2009, 2010, 2011 Free Software Foundation, Inc.
;; Author: Michael Albinus <michael.albinus@gmx.de>
;; Keywords: tools, hypermedia
;; This file is part of GNU Emacs.
-;; GNU Emacs is free software; you can redistribute it and/or modify
+;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation; either version 3, or (at your option)
-;; any later version.
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
-;; along with GNU Emacs; see the file COPYING. If not, see
-;; <http://www.gnu.org/licenses/>.
+;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
-;; This package provides an interface to the Xesam, a D-Bus based "eXtEnsible
+;; This package provides an interface to Xesam, a D-Bus based "eXtEnsible
;; Search And Metadata specification". It has been tested with
;;
;; xesam-glib 0.3.4, xesam-tools 0.6.1
;; The precondition for this package is a D-Bus aware Emacs. This is
;; configured per default, when Emacs is built on a machine running
;; D-Bus. Furthermore, there must be at least one search engine
-;; running, which support the Xesam interface. Beagle and strigi have
+;; running, which supports the Xesam interface. Beagle and strigi have
;; been tested; tracker, pinot and recoll are also said to support
;; Xesam. You can check the existence of such a search engine by
;;
;; can be selected via minibuffer completion. Afterwards, the query
;; shall be entered in the minibuffer.
+;; Search results are presented in a new buffer. This buffer has the
+;; major mode `xesam-mode', with the following keybindings:
+
+;; SPC `scroll-up'
+;; DEL `scroll-down'
+;; < `beginning-of-buffer'
+;; > `end-of-buffer'
+;; q `quit-window'
+;; z `kill-this-buffer'
+;; g `revert-buffer'
+
+;; The search results are represented by widgets. Navigation commands
+;; are the usual widget navigation commands:
+
+;; TAB `widget-forward'
+;; <backtab> `widget-backward'
+
+;; Applying RET, <down-mouse-1>, or <down-mouse-2> on a URL belonging
+;; to the widget, brings up more details of the search hit. The way,
+;; how this hit is presented, depends on the type of the hit. HTML
+;; files are opened via `browse-url'. Local files are opened in a new
+;; buffer, with highlighted search hits (highlighting can be toggled
+;; by `xesam-minor-mode' in that buffer).
+
;;; Code:
;; D-Bus support in the Emacs core can be disabled with configuration
;; Widgets are used to highlight the search results.
(require 'widget)
-
-(eval-when-compile
- (require 'wid-edit))
+(require 'wid-edit)
;; `run-at-time' is used in the signal handler.
(require 'timer)
(defgroup xesam nil
"Xesam compatible interface to search engines."
:group 'extensions
- :group 'hypermedia
+ :group 'comm
:version "23.1")
(defcustom xesam-query-type 'user-query
(const :tag "Xesam fulltext query" fulltext-query)))
(defcustom xesam-hits-per-page 20
- "Number of search hits to be displayed in the result buffer"
+ "Number of search hits to be displayed in the result buffer."
:group 'xesam
:type 'integer)
+(defface xesam-mode-line '((t :inherit mode-line-emphasis))
+ "Face to highlight mode line."
+ :group 'xesam)
+
+(defface xesam-highlight '((t :inherit match))
+ "Face to highlight query entries.
+It will be overlayed by `widget-documentation-face', so it shall
+be different at least in one face property not set in that face."
+ :group 'xesam)
+
(defvar xesam-debug nil
"Insert debug information in the help echo.")
</request>"
"The Xesam fulltext query XML.")
+(declare-function dbus-get-unique-name "dbusbind.c" (bus))
+
(defvar xesam-dbus-unique-names
(list (cons :system (dbus-get-unique-name :system))
(cons :session (dbus-get-unique-name :session)))
"Interactive query history.")
;; Pacify byte compiler.
+(defvar xesam-vendor nil)
+(make-variable-buffer-local 'xesam-vendor)
+(put 'xesam-vendor 'permanent-local t)
+
(defvar xesam-engine nil)
(defvar xesam-search nil)
(defvar xesam-type nil)
(defvar xesam-query nil)
(defvar xesam-xml-string nil)
+(defvar xesam-objects nil)
(defvar xesam-current nil)
(defvar xesam-count nil)
(defvar xesam-to nil)
+(defvar xesam-notify-function nil)
(defvar xesam-refreshing nil)
\f
(setq xesam-search-engines
(delete (assoc (car args) xesam-search-engines) xesam-search-engines)))
+(defvar dbus-debug)
+
(defun xesam-search-engines ()
"Return Xesam search engines, stored in `xesam-search-engines'.
The first search engine is the name owner of `xesam-service-search'.
;; That is not the case now, so we set it ourselves.
;; Hopefully, this will change later.
(setq hit-fields
- (cond
- ((string-equal vendor-id "Beagle")
- '("xesam:mimeType" "xesam:url"))
- ((string-equal vendor-id "Strigi")
- '("xesam:author" "xesam:cc" "xesam:cc" "xesam:charset"
- "xesam:contentType" "xesam:fileExtension" "xesam:id"
- "xesam:lineCount" "xesam:links" "xesam:mimeType" "xesam:name"
- "xesam:size" "xesam:sourceModified" "xesam:subject"
- "xesam:to" "xesam:url"))
- ((string-equal vendor-id "TrackerXesamSession")
- '("xesam:relevancyRating" "xesam:url"))
- ;; xesam-tools yahoo service.
- (t '("xesam:contentModified" "xesam:mimeType" "xesam:summary"
- "xesam:title" "xesam:url" "yahoo:displayUrl"))))
+ (case (intern vendor-id)
+ ('Beagle
+ '("xesam:mimeType" "xesam:url"))
+ ('Strigi
+ '("xesam:author" "xesam:cc" "xesam:charset"
+ "xesam:contentType" "xesam:fileExtension"
+ "xesam:id" "xesam:lineCount" "xesam:links"
+ "xesam:mimeType" "xesam:name" "xesam:size"
+ "xesam:sourceModified" "xesam:subject" "xesam:to"
+ "xesam:url"))
+ ('TrackerXesamSession
+ '("xesam:relevancyRating" "xesam:url"))
+ ('Debbugs
+ '("xesam:keyword" "xesam:owner" "xesam:title"
+ "xesam:url" "xesam:sourceModified" "xesam:mimeType"
+ "debbugs:key"))
+ ;; xesam-tools yahoo service.
+ (t '("xesam:contentModified" "xesam:mimeType" "xesam:summary"
+ "xesam:title" "xesam:url" "yahoo:displayUrl"))))
(xesam-set-property engine "hit.fields" hit-fields)
(xesam-set-property engine "hit.fields.extended" '("xesam:snippet"))
In this mode, widgets represent the search results.
\\{xesam-mode-map}
-Turning on Xesam mode runs the normal hook `xesam-mode-hook'."
+Turning on Xesam mode runs the normal hook `xesam-mode-hook'. It
+can be used to set `xesam-notify-function', which must a search
+engine specific, widget :notify function to visualize xesam:url."
+ (set (make-local-variable 'xesam-notify-function) nil)
+
;; Keymap.
(setq xesam-mode-map (copy-keymap special-mode-map))
(set-keymap-parent xesam-mode-map widget-keymap)
(set (make-local-variable 'xesam-type) "")
(set (make-local-variable 'xesam-query) "")
(set (make-local-variable 'xesam-xml-string) "")
+ (set (make-local-variable 'xesam-objects) nil)
;; `xesam-current' is the last hit put into the search buffer,
(set (make-local-variable 'xesam-current) 0)
;; `xesam-count' is the number of hits reported by the search engine.
(set (make-local-variable 'xesam-count) 0)
;; `xesam-to' is the upper hit number to be presented.
(set (make-local-variable 'xesam-to) xesam-hits-per-page)
+ ;; `xesam-notify-function' can be a search engine specific function
+ ;; to visualize xesam:url. It can be overwritten in `xesam-mode'.
+ (set (make-local-variable 'xesam-notify-function) nil)
;; `xesam-refreshing' is an indicator, whether the buffer is just
;; being updated. Needed, because `xesam-refresh-search-buffer'
;; can be triggered by an event.
(list '(20
(:eval
(list "Type: "
- (propertize xesam-type 'face 'font-lock-type-face))))
+ (propertize xesam-type 'face 'xesam-mode-line))))
'(10
(:eval
(list " Query: "
(propertize
xesam-query
- 'face 'font-lock-type-face
+ 'face 'xesam-mode-line
'help-echo (when xesam-debug xesam-xml-string)))))))
- (when (not (interactive-p))
+ (when (not (called-interactively-p 'interactive))
;; Initialize buffer.
(setq buffer-read-only t)
(let ((inhibit-read-only t))
;; It doesn't make sense to call it interactively.
(put 'xesam-mode 'disabled t)
+;; The very first buffer created with `xesam-mode' does not have the
+;; keymap etc. So we create a dummy buffer. Stupid.
+(with-temp-buffer (xesam-mode))
+
+(define-minor-mode xesam-minor-mode
+ "Toggle Xesam minor mode.
+With no argument, this command toggles the mode.
+Non-null prefix argument turns on the mode.
+Null prefix argument turns off the mode.
+
+When Xesam minor mode is enabled, all text which matches a
+previous Xesam query in this buffer is highlighted."
+ :group 'xesam
+ :init-value nil
+ :lighter " Xesam"
+ (when (local-variable-p 'xesam-query)
+ ;; Run only if the buffer is related to a Xesam search.
+ (save-excursion
+ (if xesam-minor-mode
+ ;; Highlight hits.
+ (let ((query-regexp (regexp-opt (split-string xesam-query nil t) t))
+ (case-fold-search t))
+ ;; I have no idea whether people will like setting
+ ;; `isearch-case-fold-search' and `query-regexp'. Maybe
+ ;; this shall be controlled by a custom option.
+ (unless isearch-case-fold-search (isearch-toggle-case-fold))
+ (isearch-update-ring query-regexp t)
+ ;; Create overlays.
+ (goto-char (point-min))
+ (while (re-search-forward query-regexp nil t)
+ (overlay-put
+ (make-overlay
+ (match-beginning 0) (match-end 0)) 'face 'xesam-highlight)))
+ ;; Remove overlays.
+ (dolist (ov (overlays-in (point-min) (point-max)))
+ (delete-overlay ov))))))
+
(defun xesam-buffer-name (service search)
"Return the buffer name where to present search results.
SERVICE is the D-Bus unique service name of the Xesam search engine.
SEARCH is the search identification in that engine. Both must be strings."
(format "*%s/%s*" service search))
-(defun xesam-refresh-entry (engine search)
+(defun xesam-highlight-string (string)
+ "Highlight text enclosed by <b> and </b>.
+Return propertized STRING."
+ (while (string-match "\\(.*\\)\\(<b>\\)\\(.*\\)\\(</b>\\)\\(.*\\)" string)
+ (setq string
+ (format
+ "%s%s%s"
+ (match-string 1 string)
+ (propertize (match-string 3 string) 'face 'xesam-highlight)
+ (match-string 5 string))))
+ string)
+
+(defun xesam-refresh-entry (engine entry)
"Refreshes one entry in the search buffer."
- (let* ((result
- (car
- (xesam-dbus-call-method
- :session (car engine) xesam-path-search
- xesam-interface-search "GetHits" search 1)))
- (snippet)
- ;; We must disable this for the time being; the search
- ;; engines don't return usable values so far.
-; (caaar
-; (dbus-ignore-errors
-; (xesam-dbus-call-method
-; :session (car engine) xesam-path-search
-; xesam-interface-search "GetHitData"
-; search (list xesam-current) '("snippet")))))
+ (let* ((result (nth (1- xesam-current) xesam-objects))
widget)
;; Create widget.
;; Take all results.
(dolist (field (xesam-get-cached-property engine "hit.fields"))
- (when (not (zerop (length (caar result))))
+ (when (cond
+ ((stringp (caar result)) (not (zerop (length (caar result)))))
+ ((numberp (caar result)) (not (zerop (caar result))))
+ ((caar result) t))
(when xesam-debug
(widget-put
widget :help-echo
(widget-put
widget :xesam:url (concat "file://" (widget-get widget :xesam:url))))
+ ;; Strigi returns xesam:size as string. We must fix this.
+ (when (and (widget-member widget :xesam:size)
+ (stringp (widget-get widget :xesam:size)))
+ (widget-put
+ widget :xesam:size (string-to-number (widget-get widget :xesam:url))))
+
;; First line: :tag.
(cond
((widget-member widget :xesam:title)
((widget-member widget :xesam:name)
(widget-put widget :tag (widget-get widget :xesam:name))))
+ ;; Highlight the search items.
+ (when (widget-member widget :tag)
+ (widget-put
+ widget :tag (xesam-highlight-string (widget-get widget :tag))))
+
;; Last Modified.
- (when (widget-member widget :xesam:sourceModified)
+ (when (and (widget-member widget :xesam:sourceModified)
+ (not
+ (zerop
+ (string-to-number (widget-get widget :xesam:sourceModified)))))
(widget-put
widget :tag
(format
(widget-put widget :value (widget-get widget :xesam:url))
(cond
+ ;; A search engine can set `xesam-notify-function' via
+ ;; `xesam-mode-hooks'.
+ (xesam-notify-function
+ (widget-put widget :notify xesam-notify-function))
+
;; In case of HTML, we use a URL link.
((and (widget-member widget :xesam:mimeType)
(string-equal "text/html" (widget-get widget :xesam:mimeType)))
(widget-get widget :xesam:url))))
(widget-put
widget :notify
- '(lambda (widget &rest ignore)
- (find-file
- (url-filename (url-generic-parse-url (widget-value widget))))))
+ (lambda (widget &rest ignore)
+ (let ((query xesam-query))
+ (find-file
+ (url-filename (url-generic-parse-url (widget-value widget))))
+ (set (make-local-variable 'xesam-query) query)
+ (xesam-minor-mode 1))))
(widget-put
widget :value
(url-filename (url-generic-parse-url (widget-get widget :xesam:url))))))
(widget-put widget :doc (widget-get widget :xesam:snippet))))
(when (widget-member widget :doc)
- (widget-put widget :help-echo (widget-get widget :doc))
(with-temp-buffer
- (insert (widget-get widget :doc))
+ (insert
+ (xesam-highlight-string (widget-get widget :doc)))
(fill-region-as-paragraph (point-min) (point-max))
- (widget-put widget :doc (buffer-string))))
+ (widget-put widget :doc (buffer-string)))
+ (widget-put widget :help-echo (widget-get widget :doc)))
;; Format the widget.
(widget-put
(force-mode-line-update)
(redisplay)))
+(defun xesam-get-hits (engine search hits)
+ "Retrieve hits from ENGINE."
+ (with-current-buffer (xesam-buffer-name (car engine) search)
+ (setq xesam-objects
+ (append xesam-objects
+ (xesam-dbus-call-method
+ :session (car engine) xesam-path-search
+ xesam-interface-search "GetHits" search hits)))))
+
(defun xesam-refresh-search-buffer (engine search)
"Refreshes the buffer, presenting results of SEARCH."
(with-current-buffer (xesam-buffer-name (car engine) search)
;; Work only if nobody else is here.
- (unless xesam-refreshing
+ (unless (or xesam-refreshing (>= xesam-current xesam-to))
(setq xesam-refreshing t)
(unwind-protect
- (let (widget updated)
- ;; Add all result widgets. The upper boundary is always
- ;; computed, because new hits might have arrived while
- ;; running.
+ (let (widget)
+
+ ;; Retrieve needed hits for visualization.
+ (while (> (min xesam-to xesam-count) (length xesam-objects))
+ (xesam-get-hits
+ engine search
+ (min xesam-hits-per-page
+ (- (min xesam-to xesam-count) (length xesam-objects)))))
+
+ ;; Add all result widgets.
(while (< xesam-current (min xesam-to xesam-count))
- (setq updated t
- xesam-current (1+ xesam-current))
+ (setq xesam-current (1+ xesam-current))
(xesam-refresh-entry engine search))
;; Add "NEXT" widget.
- (when (and updated (> xesam-count xesam-to))
+ (when (> xesam-count xesam-to)
(goto-char (point-max))
(widget-create
'link
:notify
- '(lambda (widget &rest ignore)
- (setq xesam-to (+ xesam-to xesam-hits-per-page))
- (widget-delete widget)
- (xesam-refresh-search-buffer xesam-engine xesam-search))
+ (lambda (widget &rest ignore)
+ (setq xesam-to (+ xesam-to xesam-hits-per-page))
+ (widget-delete widget)
+ (xesam-refresh-search-buffer xesam-engine xesam-search))
"NEXT")
+ (widget-beginning-of-line))
+
+ ;; Prefetch next hits.
+ (when (> (min (+ xesam-hits-per-page xesam-to) xesam-count)
+ (length xesam-objects))
+ (xesam-get-hits
+ engine search
+ (min xesam-hits-per-page
+ (- (min (+ xesam-hits-per-page xesam-to) xesam-count)
+ (length xesam-objects)))))
+
+ ;; Add "DONE" widget.
+ (when (= xesam-current xesam-count)
+ (goto-char (point-max))
+ (widget-create 'link :notify 'ignore "DONE")
(widget-beginning-of-line)))
;; Return with save settings.
((string-equal member "SearchDone")
(setq mode-line-process
- (propertize " Done" 'face 'font-lock-type-face))
+ (propertize " Done" 'face 'xesam-mode-line))
(force-mode-line-update)))))))
+(defun xesam-kill-buffer-function ()
+ "Send the CloseSearch indication."
+ (when (and (eq major-mode 'xesam-mode) (stringp xesam-search))
+ (ignore-errors ;; The D-Bus service could have disappeared.
+ (xesam-dbus-call-method
+ :session (car xesam-engine) xesam-path-search
+ xesam-interface-search "CloseSearch" xesam-search))))
+
(defun xesam-new-search (engine type query)
"Create a new search session.
ENGINE identifies the search engine. TYPE is the query type, it
(xml-string
(format
(if (eq type 'user-query) xesam-user-query xesam-fulltext-query)
- query))
+ (url-insert-entities-in-string query)))
(search (xesam-dbus-call-method
:session service xesam-path-search
xesam-interface-search "NewSearch" session xml-string)))
(with-current-buffer
(generate-new-buffer (xesam-buffer-name service search))
(switch-to-buffer-other-window (current-buffer))
+ ;; Inialize buffer with `xesam-mode'. `xesam-vendor' must be
+ ;; set before calling `xesam-mode', because we want to give the
+ ;; hook functions a chance to identify their search engine.
+ (setq xesam-vendor (xesam-get-cached-property engine "vendor.id"))
(xesam-mode)
(setq xesam-engine engine
xesam-search search
xesam-type (symbol-name type)
xesam-query query
xesam-xml-string xml-string
+ xesam-objects nil
;; The buffer identification shall indicate the search
;; engine. The `help-echo' property is used for debug
;; information, when applicable.
mode-line-buffer-identification
(if (not xesam-debug)
- (list
- 12 (propertized-buffer-identification
- (xesam-get-cached-property engine "vendor.id")))
+ (list 12 (propertized-buffer-identification xesam-vendor))
(propertize
- (xesam-get-cached-property engine "vendor.id")
+ xesam-vendor
'help-echo
(mapconcat
- '(lambda (x)
- (format "%s: %s" x (xesam-get-cached-property engine x)))
+ (lambda (x)
+ (format "%s: %s" x (xesam-get-cached-property engine x)))
'("vendor.id" "vendor.version" "vendor.display" "vendor.xesam"
"vendor.ontology.fields" "vendor.ontology.contents"
"vendor.ontology.sources" "vendor.extensions"
"vendor.ontologies" "vendor.maxhits")
"\n"))))
- (force-mode-line-update))
+ (add-hook 'kill-buffer-hook 'xesam-kill-buffer-function)
+ (force-mode-line-update))
;; Start the search.
(xesam-dbus-call-method
;; Return search id.
search))
+;;;###autoload
(defun xesam-search (engine query)
"Perform an interactive search.
ENGINE is the Xesam search engine to be applied, it must be one of the
(xesam-search (car (xesam-search-engines)) \"emacs\")"
(interactive
(let* ((vendors (mapcar
- '(lambda (x) (xesam-get-cached-property x "vendor.display"))
+ (lambda (x) (xesam-get-cached-property x "vendor.display"))
(xesam-search-engines)))
(vendor
(if (> (length vendors) 1)
;;; TODO:
-;; * Solve error, that xesam-mode does not work the very first time.
-;; * Retrieve several results at once.
-;; * Retrieve hits for the next page in advance.
+;; * Buffer highlighting needs better analysis of query string.
+;; * Accept input while retrieving prefetched hits. `run-at-time'?
;; * With prefix, let's choose search engine.
;; * Minibuffer completion for user queries.
;; * `revert-buffer-function' implementation.
-;; * Close search when search buffer is killed.
;;
;; * Mid term
;; - If available, use ontologies for field selection.
;; - Search engines for Emacs bugs database, wikipedia, google,
;; yahoo, ebay, ...
+;; - Construct complex queries via widgets, like in mairix.el.
;; arch-tag: 7fb9fc6c-c2ff-4bc7-bb42-bacb80cce2b2
;;; xesam.el ends here