emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/xeft afc8a69a52 01/55: init


From: ELPA Syncer
Subject: [elpa] externals/xeft afc8a69a52 01/55: init
Date: Fri, 13 Jan 2023 23:58:36 -0500 (EST)

branch: externals/xeft
commit afc8a69a526ab7662bf68eb87ee76d5547116bb0
Author: Yuan Fu <casouri@gmail.com>
Commit: Yuan Fu <casouri@gmail.com>

    init
---
 Makefile       |  12 +
 README.md      |  75 ++++++
 emacs-module.h | 763 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 gitignore      |   2 +
 xeft-module.cc | 445 +++++++++++++++++++++++++++++++++
 xeft.el        | 421 +++++++++++++++++++++++++++++++
 6 files changed, 1718 insertions(+)

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000..85ea3a839a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,12 @@
+.POSIX:
+PREFIX = /usr/local
+CXX = g++
+CXXFLAGS = -I$(PREFIX)/include
+LDFLAGS = -L$(PREFIX)/lib
+LDLIBS = -lxapian
+
+xeft-module.so: xeft-module.cc
+       $(CXX) -shared $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) $< -o $@
+
+clean:
+       rm -f *.so *.o
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..937c05185b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+# What is this
+
+Xeft is
+
+1. A dynamic module that exposes a very basic indexing feature to
+   Emacs Lisp, that lets you index and search a text files very fast.
+   I tried to use grep and ripgrep to build my note-searching interface,
+   but they are too slow; with Xeft I can search on every key press.
+
+2. A note taking interface like Deft, built on the dynamic module.
+
+# How to use the dynamic module
+
+Because it’s so basic, the dynamic module is very easy to use and
+also very flexible. To index files, use
+
+```emacs-lisp
+(dolist (file (directory-files "my-note-dir"))
+  (xeft-reindex-file file dbpath))
+```
+
+This indexes each file in `my-note-dir`, saving them to the database
+at `dbpath`. If the database doesn’t exist yet, it is created.
+
+To search for a term, use
+
+```emacs-lisp
+(xeft-query-term "search term" dbpath 0 10)
+```
+
+This returns a list of paths of the files that contains `search term`,
+ranked by relevance. The `0` and `10` means “return 10 results
+starting from the 0th place”, it is essentially used for paging. If
+you want all the result, use `0` and `999999`.
+
+When a file is modified, call `xeft-reindex-file` again on that file.
+If a file is removed, you don’t need to remove it from the database,
+it will be automatically removed. If the file has been indexed and
+haven’t been modified, `xeft-reindex-file` is (kind of) a no-op (i.e.
+fast).
+
+Both file path and database path must be absolute path.
+
+# How to use the note-taking interface
+
+It is essentially the same as [Zeft](https://github.com/casouri/zeft).
+
+# How to build the dynamic module
+
+To build the module, you need to have Xapian installed. On Mac, it can
+be installed with macports by
+
+```shell
+sudo port install xapian-core
+```
+
+Then, build the module by
+
+```shell
+make PREFIX=/opt/local
+```
+
+Here `/opt/local` is the default prefix of macports.
+
+# Disclaimer
+
+Since its a dynamic module, if Xeft goes wrong, it will crash Emacs.
+
+# notdeft
+
+Many thanks to the author of notdeft. I don’t really know C++ or
+Xapian, without reading his code I wouldn’t be able to write Xeft.
+
+Also, if you want a more powerful searching experience, you will be
+happier using notdeft instead.
diff --git a/emacs-module.h b/emacs-module.h
new file mode 100644
index 0000000000..1185c06f45
--- /dev/null
+++ b/emacs-module.h
@@ -0,0 +1,763 @@
+/* emacs-module.h - GNU Emacs module API.
+
+Copyright (C) 2015-2021 Free Software Foundation, Inc.
+
+This file is part of GNU Emacs.
+
+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 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
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.  */
+
+/*
+This file defines the Emacs module API.  Please see the chapter
+`Dynamic Modules' in the GNU Emacs Lisp Reference Manual for
+information how to write modules and use this header file.
+*/
+
+#ifndef EMACS_MODULE_H
+#define EMACS_MODULE_H
+
+#include <stddef.h>
+#include <stdint.h>
+#include <time.h>
+
+#ifndef __cplusplus
+#include <stdbool.h>
+#endif
+
+#define EMACS_MAJOR_VERSION 28
+
+#if defined __cplusplus && __cplusplus >= 201103L
+# define EMACS_NOEXCEPT noexcept
+#else
+# define EMACS_NOEXCEPT
+#endif
+
+#if defined __cplusplus && __cplusplus >= 201703L
+# define EMACS_NOEXCEPT_TYPEDEF noexcept
+#else
+# define EMACS_NOEXCEPT_TYPEDEF
+#endif
+
+#if 3 < __GNUC__ + (3 <= __GNUC_MINOR__)
+# define EMACS_ATTRIBUTE_NONNULL(...) \
+   __attribute__ ((__nonnull__ (__VA_ARGS__)))
+#elif (defined __has_attribute \
+       && (!defined __clang_minor__ \
+          || 3 < __clang_major__ + (5 <= __clang_minor__)))
+# if __has_attribute (__nonnull__)
+#  define EMACS_ATTRIBUTE_NONNULL(...) \
+    __attribute__ ((__nonnull__ (__VA_ARGS__)))
+# endif
+#endif
+#ifndef EMACS_ATTRIBUTE_NONNULL
+# define EMACS_ATTRIBUTE_NONNULL(...)
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Current environment.  */
+typedef struct emacs_env_28 emacs_env;
+
+/* Opaque pointer representing an Emacs Lisp value.
+   BEWARE: Do not assume NULL is a valid value!  */
+typedef struct emacs_value_tag *emacs_value;
+
+enum { emacs_variadic_function = -2 };
+
+/* Struct passed to a module init function (emacs_module_init).  */
+struct emacs_runtime
+{
+  /* Structure size (for version checking).  */
+  ptrdiff_t size;
+
+  /* Private data; users should not touch this.  */
+  struct emacs_runtime_private *private_members;
+
+  /* Return an environment pointer.  */
+  emacs_env *(*get_environment) (struct emacs_runtime *runtime)
+    EMACS_ATTRIBUTE_NONNULL (1);
+};
+
+/* Type aliases for function pointer types used in the module API.
+   Note that we don't use these aliases directly in the API to be able
+   to mark the function arguments as 'noexcept' before C++20.
+   However, users can use them if they want.  */
+
+/* Function prototype for the module Lisp functions.  These must not
+   throw C++ exceptions.  */
+typedef emacs_value (*emacs_function) (emacs_env *env, ptrdiff_t nargs,
+                                       emacs_value *args,
+                                       void *data)
+  EMACS_NOEXCEPT_TYPEDEF EMACS_ATTRIBUTE_NONNULL (1);
+
+/* Function prototype for module user-pointer and function finalizers.
+   These must not throw C++ exceptions.  */
+typedef void (*emacs_finalizer) (void *data) EMACS_NOEXCEPT_TYPEDEF;
+
+/* Possible Emacs function call outcomes.  */
+enum emacs_funcall_exit
+{
+  /* Function has returned normally.  */
+  emacs_funcall_exit_return = 0,
+
+  /* Function has signaled an error using `signal'.  */
+  emacs_funcall_exit_signal = 1,
+
+  /* Function has exit using `throw'.  */
+  emacs_funcall_exit_throw = 2
+};
+
+/* Possible return values for emacs_env.process_input.  */
+enum emacs_process_input_result
+{
+  /* Module code may continue  */
+  emacs_process_input_continue = 0,
+
+  /* Module code should return control to Emacs as soon as possible.  */
+  emacs_process_input_quit = 1
+};
+
+/* Define emacs_limb_t so that it is likely to match GMP's mp_limb_t.
+   This micro-optimization can help modules that use mpz_export and
+   mpz_import, which operate more efficiently on mp_limb_t.  It's OK
+   (if perhaps a bit slower) if the two types do not match, and
+   modules shouldn't rely on the two types matching.  */
+typedef size_t emacs_limb_t;
+#define EMACS_LIMB_MAX SIZE_MAX
+
+struct emacs_env_25
+{
+  /* Structure size (for version checking).  */
+  ptrdiff_t size;
+
+  /* Private data; users should not touch this.  */
+  struct emacs_env_private *private_members;
+
+  /* Memory management.  */
+
+  emacs_value (*make_global_ref) (emacs_env *env, emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*free_global_ref) (emacs_env *env, emacs_value global_value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Non-local exit handling.  */
+
+  enum emacs_funcall_exit (*non_local_exit_check) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*non_local_exit_clear) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  enum emacs_funcall_exit (*non_local_exit_get)
+    (emacs_env *env, emacs_value *symbol, emacs_value *data)
+    EMACS_ATTRIBUTE_NONNULL(1, 2, 3);
+
+  void (*non_local_exit_signal) (emacs_env *env,
+                                emacs_value symbol, emacs_value data)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*non_local_exit_throw) (emacs_env *env,
+                               emacs_value tag, emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Function registration.  */
+
+  emacs_value (*make_function) (emacs_env *env,
+                               ptrdiff_t min_arity,
+                               ptrdiff_t max_arity,
+                               emacs_value (*func) (emacs_env *env,
+                                                     ptrdiff_t nargs,
+                                                     emacs_value* args,
+                                                     void *data)
+                                 EMACS_NOEXCEPT
+                                  EMACS_ATTRIBUTE_NONNULL(1),
+                               const char *docstring,
+                               void *data)
+    EMACS_ATTRIBUTE_NONNULL(1, 4);
+
+  emacs_value (*funcall) (emacs_env *env,
+                          emacs_value func,
+                          ptrdiff_t nargs,
+                          emacs_value* args)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*intern) (emacs_env *env, const char *name)
+    EMACS_ATTRIBUTE_NONNULL(1, 2);
+
+  /* Type conversion.  */
+
+  emacs_value (*type_of) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  bool (*is_not_nil) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  bool (*eq) (emacs_env *env, emacs_value a, emacs_value b)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  intmax_t (*extract_integer) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*make_integer) (emacs_env *env, intmax_t n)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  double (*extract_float) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*make_float) (emacs_env *env, double d)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Copy the content of the Lisp string VALUE to BUFFER as an utf8
+     null-terminated string.
+
+     SIZE must point to the total size of the buffer.  If BUFFER is
+     NULL or if SIZE is not big enough, write the required buffer size
+     to SIZE and return true.
+
+     Note that SIZE must include the last null byte (e.g. "abc" needs
+     a buffer of size 4).
+
+     Return true if the string was successfully copied.  */
+
+  bool (*copy_string_contents) (emacs_env *env,
+                                emacs_value value,
+                                char *buf,
+                                ptrdiff_t *len)
+    EMACS_ATTRIBUTE_NONNULL(1, 4);
+
+  /* Create a Lisp string from a utf8 encoded string.  */
+  emacs_value (*make_string) (emacs_env *env,
+                             const char *str, ptrdiff_t len)
+    EMACS_ATTRIBUTE_NONNULL(1, 2);
+
+  /* Embedded pointer type.  */
+  emacs_value (*make_user_ptr) (emacs_env *env,
+                               void (*fin) (void *) EMACS_NOEXCEPT,
+                               void *ptr)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void *(*get_user_ptr) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+  void (*set_user_ptr) (emacs_env *env, emacs_value arg, void *ptr)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*(*get_user_finalizer) (emacs_env *env, emacs_value uptr))
+    (void *) EMACS_NOEXCEPT EMACS_ATTRIBUTE_NONNULL(1);
+  void (*set_user_finalizer) (emacs_env *env, emacs_value arg,
+                             void (*fin) (void *) EMACS_NOEXCEPT)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Vector functions.  */
+  emacs_value (*vec_get) (emacs_env *env, emacs_value vector, ptrdiff_t index)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*vec_set) (emacs_env *env, emacs_value vector, ptrdiff_t index,
+                  emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  ptrdiff_t (*vec_size) (emacs_env *env, emacs_value vector)
+    EMACS_ATTRIBUTE_NONNULL(1);
+};
+
+struct emacs_env_26
+{
+  /* Structure size (for version checking).  */
+  ptrdiff_t size;
+
+  /* Private data; users should not touch this.  */
+  struct emacs_env_private *private_members;
+
+  /* Memory management.  */
+
+  emacs_value (*make_global_ref) (emacs_env *env, emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*free_global_ref) (emacs_env *env, emacs_value global_value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Non-local exit handling.  */
+
+  enum emacs_funcall_exit (*non_local_exit_check) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*non_local_exit_clear) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  enum emacs_funcall_exit (*non_local_exit_get)
+    (emacs_env *env, emacs_value *symbol, emacs_value *data)
+    EMACS_ATTRIBUTE_NONNULL(1, 2, 3);
+
+  void (*non_local_exit_signal) (emacs_env *env,
+                                emacs_value symbol, emacs_value data)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*non_local_exit_throw) (emacs_env *env,
+                               emacs_value tag, emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Function registration.  */
+
+  emacs_value (*make_function) (emacs_env *env,
+                               ptrdiff_t min_arity,
+                               ptrdiff_t max_arity,
+                               emacs_value (*func) (emacs_env *env,
+                                                     ptrdiff_t nargs,
+                                                     emacs_value* args,
+                                                     void *data)
+                                 EMACS_NOEXCEPT
+                                  EMACS_ATTRIBUTE_NONNULL(1),
+                               const char *docstring,
+                               void *data)
+    EMACS_ATTRIBUTE_NONNULL(1, 4);
+
+  emacs_value (*funcall) (emacs_env *env,
+                          emacs_value func,
+                          ptrdiff_t nargs,
+                          emacs_value* args)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*intern) (emacs_env *env, const char *name)
+    EMACS_ATTRIBUTE_NONNULL(1, 2);
+
+  /* Type conversion.  */
+
+  emacs_value (*type_of) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  bool (*is_not_nil) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  bool (*eq) (emacs_env *env, emacs_value a, emacs_value b)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  intmax_t (*extract_integer) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*make_integer) (emacs_env *env, intmax_t n)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  double (*extract_float) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*make_float) (emacs_env *env, double d)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Copy the content of the Lisp string VALUE to BUFFER as an utf8
+     null-terminated string.
+
+     SIZE must point to the total size of the buffer.  If BUFFER is
+     NULL or if SIZE is not big enough, write the required buffer size
+     to SIZE and return true.
+
+     Note that SIZE must include the last null byte (e.g. "abc" needs
+     a buffer of size 4).
+
+     Return true if the string was successfully copied.  */
+
+  bool (*copy_string_contents) (emacs_env *env,
+                                emacs_value value,
+                                char *buf,
+                                ptrdiff_t *len)
+    EMACS_ATTRIBUTE_NONNULL(1, 4);
+
+  /* Create a Lisp string from a utf8 encoded string.  */
+  emacs_value (*make_string) (emacs_env *env,
+                             const char *str, ptrdiff_t len)
+    EMACS_ATTRIBUTE_NONNULL(1, 2);
+
+  /* Embedded pointer type.  */
+  emacs_value (*make_user_ptr) (emacs_env *env,
+                               void (*fin) (void *) EMACS_NOEXCEPT,
+                               void *ptr)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void *(*get_user_ptr) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+  void (*set_user_ptr) (emacs_env *env, emacs_value arg, void *ptr)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*(*get_user_finalizer) (emacs_env *env, emacs_value uptr))
+    (void *) EMACS_NOEXCEPT EMACS_ATTRIBUTE_NONNULL(1);
+  void (*set_user_finalizer) (emacs_env *env, emacs_value arg,
+                             void (*fin) (void *) EMACS_NOEXCEPT)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Vector functions.  */
+  emacs_value (*vec_get) (emacs_env *env, emacs_value vector, ptrdiff_t index)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*vec_set) (emacs_env *env, emacs_value vector, ptrdiff_t index,
+                  emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  ptrdiff_t (*vec_size) (emacs_env *env, emacs_value vector)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Returns whether a quit is pending.  */
+  bool (*should_quit) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+};
+
+struct emacs_env_27
+{
+  /* Structure size (for version checking).  */
+  ptrdiff_t size;
+
+  /* Private data; users should not touch this.  */
+  struct emacs_env_private *private_members;
+
+  /* Memory management.  */
+
+  emacs_value (*make_global_ref) (emacs_env *env, emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*free_global_ref) (emacs_env *env, emacs_value global_value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Non-local exit handling.  */
+
+  enum emacs_funcall_exit (*non_local_exit_check) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*non_local_exit_clear) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  enum emacs_funcall_exit (*non_local_exit_get)
+    (emacs_env *env, emacs_value *symbol, emacs_value *data)
+    EMACS_ATTRIBUTE_NONNULL(1, 2, 3);
+
+  void (*non_local_exit_signal) (emacs_env *env,
+                                emacs_value symbol, emacs_value data)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*non_local_exit_throw) (emacs_env *env,
+                               emacs_value tag, emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Function registration.  */
+
+  emacs_value (*make_function) (emacs_env *env,
+                               ptrdiff_t min_arity,
+                               ptrdiff_t max_arity,
+                               emacs_value (*func) (emacs_env *env,
+                                                     ptrdiff_t nargs,
+                                                     emacs_value* args,
+                                                     void *data)
+                                 EMACS_NOEXCEPT
+                                  EMACS_ATTRIBUTE_NONNULL(1),
+                               const char *docstring,
+                               void *data)
+    EMACS_ATTRIBUTE_NONNULL(1, 4);
+
+  emacs_value (*funcall) (emacs_env *env,
+                          emacs_value func,
+                          ptrdiff_t nargs,
+                          emacs_value* args)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*intern) (emacs_env *env, const char *name)
+    EMACS_ATTRIBUTE_NONNULL(1, 2);
+
+  /* Type conversion.  */
+
+  emacs_value (*type_of) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  bool (*is_not_nil) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  bool (*eq) (emacs_env *env, emacs_value a, emacs_value b)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  intmax_t (*extract_integer) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*make_integer) (emacs_env *env, intmax_t n)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  double (*extract_float) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*make_float) (emacs_env *env, double d)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Copy the content of the Lisp string VALUE to BUFFER as an utf8
+     null-terminated string.
+
+     SIZE must point to the total size of the buffer.  If BUFFER is
+     NULL or if SIZE is not big enough, write the required buffer size
+     to SIZE and return true.
+
+     Note that SIZE must include the last null byte (e.g. "abc" needs
+     a buffer of size 4).
+
+     Return true if the string was successfully copied.  */
+
+  bool (*copy_string_contents) (emacs_env *env,
+                                emacs_value value,
+                                char *buf,
+                                ptrdiff_t *len)
+    EMACS_ATTRIBUTE_NONNULL(1, 4);
+
+  /* Create a Lisp string from a utf8 encoded string.  */
+  emacs_value (*make_string) (emacs_env *env,
+                             const char *str, ptrdiff_t len)
+    EMACS_ATTRIBUTE_NONNULL(1, 2);
+
+  /* Embedded pointer type.  */
+  emacs_value (*make_user_ptr) (emacs_env *env,
+                               void (*fin) (void *) EMACS_NOEXCEPT,
+                               void *ptr)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void *(*get_user_ptr) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+  void (*set_user_ptr) (emacs_env *env, emacs_value arg, void *ptr)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*(*get_user_finalizer) (emacs_env *env, emacs_value uptr))
+    (void *) EMACS_NOEXCEPT EMACS_ATTRIBUTE_NONNULL(1);
+  void (*set_user_finalizer) (emacs_env *env, emacs_value arg,
+                             void (*fin) (void *) EMACS_NOEXCEPT)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Vector functions.  */
+  emacs_value (*vec_get) (emacs_env *env, emacs_value vector, ptrdiff_t index)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*vec_set) (emacs_env *env, emacs_value vector, ptrdiff_t index,
+                  emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  ptrdiff_t (*vec_size) (emacs_env *env, emacs_value vector)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Returns whether a quit is pending.  */
+  bool (*should_quit) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Processes pending input events and returns whether the module
+     function should quit.  */
+  enum emacs_process_input_result (*process_input) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  struct timespec (*extract_time) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  emacs_value (*make_time) (emacs_env *env, struct timespec time)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  bool (*extract_big_integer) (emacs_env *env, emacs_value arg, int *sign,
+                               ptrdiff_t *count, emacs_limb_t *magnitude)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  emacs_value (*make_big_integer) (emacs_env *env, int sign, ptrdiff_t count,
+                                   const emacs_limb_t *magnitude)
+    EMACS_ATTRIBUTE_NONNULL (1);
+};
+
+struct emacs_env_28
+{
+  /* Structure size (for version checking).  */
+  ptrdiff_t size;
+
+  /* Private data; users should not touch this.  */
+  struct emacs_env_private *private_members;
+
+  /* Memory management.  */
+
+  emacs_value (*make_global_ref) (emacs_env *env, emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*free_global_ref) (emacs_env *env, emacs_value global_value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Non-local exit handling.  */
+
+  enum emacs_funcall_exit (*non_local_exit_check) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*non_local_exit_clear) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  enum emacs_funcall_exit (*non_local_exit_get)
+    (emacs_env *env, emacs_value *symbol, emacs_value *data)
+    EMACS_ATTRIBUTE_NONNULL(1, 2, 3);
+
+  void (*non_local_exit_signal) (emacs_env *env,
+                                emacs_value symbol, emacs_value data)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*non_local_exit_throw) (emacs_env *env,
+                               emacs_value tag, emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Function registration.  */
+
+  emacs_value (*make_function) (emacs_env *env,
+                               ptrdiff_t min_arity,
+                               ptrdiff_t max_arity,
+                               emacs_value (*func) (emacs_env *env,
+                                                     ptrdiff_t nargs,
+                                                     emacs_value* args,
+                                                     void *data)
+                                 EMACS_NOEXCEPT
+                                  EMACS_ATTRIBUTE_NONNULL(1),
+                               const char *docstring,
+                               void *data)
+    EMACS_ATTRIBUTE_NONNULL(1, 4);
+
+  emacs_value (*funcall) (emacs_env *env,
+                          emacs_value func,
+                          ptrdiff_t nargs,
+                          emacs_value* args)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*intern) (emacs_env *env, const char *name)
+    EMACS_ATTRIBUTE_NONNULL(1, 2);
+
+  /* Type conversion.  */
+
+  emacs_value (*type_of) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  bool (*is_not_nil) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  bool (*eq) (emacs_env *env, emacs_value a, emacs_value b)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  intmax_t (*extract_integer) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*make_integer) (emacs_env *env, intmax_t n)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  double (*extract_float) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  emacs_value (*make_float) (emacs_env *env, double d)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Copy the content of the Lisp string VALUE to BUFFER as an utf8
+     null-terminated string.
+
+     SIZE must point to the total size of the buffer.  If BUFFER is
+     NULL or if SIZE is not big enough, write the required buffer size
+     to SIZE and return true.
+
+     Note that SIZE must include the last null byte (e.g. "abc" needs
+     a buffer of size 4).
+
+     Return true if the string was successfully copied.  */
+
+  bool (*copy_string_contents) (emacs_env *env,
+                                emacs_value value,
+                                char *buf,
+                                ptrdiff_t *len)
+    EMACS_ATTRIBUTE_NONNULL(1, 4);
+
+  /* Create a Lisp string from a utf8 encoded string.  */
+  emacs_value (*make_string) (emacs_env *env,
+                             const char *str, ptrdiff_t len)
+    EMACS_ATTRIBUTE_NONNULL(1, 2);
+
+  /* Embedded pointer type.  */
+  emacs_value (*make_user_ptr) (emacs_env *env,
+                               void (*fin) (void *) EMACS_NOEXCEPT,
+                               void *ptr)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void *(*get_user_ptr) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL(1);
+  void (*set_user_ptr) (emacs_env *env, emacs_value arg, void *ptr)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*(*get_user_finalizer) (emacs_env *env, emacs_value uptr))
+    (void *) EMACS_NOEXCEPT EMACS_ATTRIBUTE_NONNULL(1);
+  void (*set_user_finalizer) (emacs_env *env, emacs_value arg,
+                             void (*fin) (void *) EMACS_NOEXCEPT)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Vector functions.  */
+  emacs_value (*vec_get) (emacs_env *env, emacs_value vector, ptrdiff_t index)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  void (*vec_set) (emacs_env *env, emacs_value vector, ptrdiff_t index,
+                  emacs_value value)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  ptrdiff_t (*vec_size) (emacs_env *env, emacs_value vector)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Returns whether a quit is pending.  */
+  bool (*should_quit) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL(1);
+
+  /* Processes pending input events and returns whether the module
+     function should quit.  */
+  enum emacs_process_input_result (*process_input) (emacs_env *env)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  struct timespec (*extract_time) (emacs_env *env, emacs_value arg)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  emacs_value (*make_time) (emacs_env *env, struct timespec time)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  bool (*extract_big_integer) (emacs_env *env, emacs_value arg, int *sign,
+                               ptrdiff_t *count, emacs_limb_t *magnitude)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  emacs_value (*make_big_integer) (emacs_env *env, int sign, ptrdiff_t count,
+                                   const emacs_limb_t *magnitude)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  /* Add module environment functions newly added in Emacs 28 here.
+     Before Emacs 28 is released, remove this comment and start
+     module-env-29.h on the master branch.  */
+
+  void (*(*EMACS_ATTRIBUTE_NONNULL (1)
+            get_function_finalizer) (emacs_env *env,
+                                     emacs_value arg)) (void *) EMACS_NOEXCEPT;
+
+  void (*set_function_finalizer) (emacs_env *env, emacs_value arg,
+                                  void (*fin) (void *) EMACS_NOEXCEPT)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  int (*open_channel) (emacs_env *env, emacs_value pipe_process)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  void (*make_interactive) (emacs_env *env, emacs_value function,
+                            emacs_value spec)
+    EMACS_ATTRIBUTE_NONNULL (1);
+
+  /* Create a unibyte Lisp string from a string.  */
+  emacs_value (*make_unibyte_string) (emacs_env *env,
+                                     const char *str, ptrdiff_t len)
+    EMACS_ATTRIBUTE_NONNULL(1, 2);
+};
+
+/* Every module should define a function as follows.  */
+extern int emacs_module_init (struct emacs_runtime *runtime)
+  EMACS_NOEXCEPT
+  EMACS_ATTRIBUTE_NONNULL (1);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* EMACS_MODULE_H */
diff --git a/gitignore b/gitignore
new file mode 100644
index 0000000000..e2d2375a62
--- /dev/null
+++ b/gitignore
@@ -0,0 +1,2 @@
+*.so
+*.o
\ No newline at end of file
diff --git a/xeft-module.cc b/xeft-module.cc
new file mode 100644
index 0000000000..22152e6bd7
--- /dev/null
+++ b/xeft-module.cc
@@ -0,0 +1,445 @@
+#include <string>
+#include <cstring>
+#include <iostream>
+#include <fstream>
+#include <vector>
+#include <exception>
+#include <iterator>
+
+#include <stdlib.h>
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <xapian.h>
+
+#include "emacs-module.h"
+
+using namespace std;
+
+int plugin_is_GPL_compatible;
+
+/*** Xapian stuff */
+
+static const Xapian::valueno DOC_MTIME = 0;
+static const Xapian::valueno DOC_FILEPATH = 1;
+
+static Xapian::WritableDatabase database;
+static string cached_dbpath = "";
+
+class xeft_cannot_open_file: public exception {};
+
+// Reindex the file at PATH, using database at DBPATH. Throws
+// cannot_open_file. Both path must be absolute. Normally only reindex
+// if file has change since last index, if FORCE is true, always
+// reindex. Return true if re-indexed, return false if didn’t.
+// LANG is the language used by the stemmer.
+// Possible langauges:
+// https://xapian.org/docs/apidoc/html/classXapian_1_1Stem.html
+static bool
+reindex_file
+(string path, string dbpath, string lang = "en", bool force = false)
+{
+  // Check for mtime.
+  struct stat st;
+  time_t file_mtime;
+  off_t file_size;
+  if (stat (path.c_str(), &st) == 0)
+    {
+      file_mtime = st.st_mtime;
+      file_size = st.st_size;
+    }
+  else
+    {
+      throw xeft_cannot_open_file();
+    }
+
+  // Even though the document says that database object only carries a
+  // pointer to the actual object, it is still not cheap enough. By
+  // using this cache, we get much better performance when reindexing
+  // hundreds of files, which most are no-op because they hasn’t been
+  // modified.
+  if (dbpath != cached_dbpath)
+    {
+      database = Xapian::WritableDatabase
+        (dbpath, Xapian::DB_CREATE_OR_OPEN);
+      cached_dbpath = dbpath;
+    }
+  // Track doc with file path as "id". See
+  // 
https://getting-started-with-xapian.readthedocs.io/en/latest/practical_example/indexing/updating_the_database.html
+  string termID = 'Q' + path;
+  Xapian::PostingIterator it_begin = database.postlist_begin (termID);
+  Xapian::PostingIterator it_end = database.postlist_end (termID);
+  bool has_doc = it_begin != it_end;
+  time_t db_mtime;
+  if (has_doc)
+    {
+      Xapian::Document db_doc = database.get_document(*it_begin);
+      db_mtime = (time_t) stoi (db_doc.get_value (DOC_MTIME));
+    }
+
+  // Need re-index.
+  if (!has_doc || (has_doc && db_mtime < file_mtime) || force)
+    {
+      // Get the file content.
+      // REF: 
https://stackoverflow.com/questions/2912520/read-file-contents-into-a-string-in-c
+      ifstream infile (path);
+      string content ((istreambuf_iterator<char>(infile)),
+                      (istreambuf_iterator<char>()));
+      // Create the indexer.
+      Xapian::TermGenerator indexer;
+      Xapian::Stem stemmer (lang);
+      indexer.set_stemmer (stemmer);
+      indexer.set_stemming_strategy
+        (Xapian::TermGenerator::STEM_SOME);
+      // Support CJK.
+      indexer.set_flags (Xapian::TermGenerator::FLAG_CJK_NGRAM);
+      // Index file content.
+      Xapian::Document new_doc;
+      indexer.set_document (new_doc);
+      indexer.index_text (content);
+      // Set doc info.
+      new_doc.add_boolean_term (termID);
+      // We store the path in value, no need to use set_data.
+      new_doc.add_value (DOC_FILEPATH, path);
+      new_doc.add_value (DOC_MTIME, (string) to_string (file_mtime));
+      database.replace_document (termID, new_doc);
+      return true;
+    }
+  else
+    {
+      return false;
+    }
+}
+
+// Query TERM in the databse at DBPATH. OFFSET and PAGE_SIZE is for
+// paging, see the docstring for the lisp function. If a file in the
+// result doesn’t exist anymore, it is removed from the database.
+// LANG is the language used by the stemmer.
+// Possible langauges:
+// https://xapian.org/docs/apidoc/html/classXapian_1_1Stem.html
+static vector<string>
+query_term
+(string term, string dbpath, int offset, int page_size, string lang = "en")
+{
+  // See reindex_file for the reason for caching the database object.
+  if (dbpath != cached_dbpath)
+    {
+      database = Xapian::WritableDatabase
+        (dbpath, Xapian::DB_CREATE_OR_OPEN);
+      cached_dbpath = dbpath;
+    }
+
+  Xapian::QueryParser parser;
+  Xapian::Stem stemmer (lang);
+  parser.set_stemmer (stemmer);
+  parser.set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
+
+  Xapian::Query query;
+  try
+    {
+      query = parser.parse_query
+        // Support CJK.
+        (term, Xapian::QueryParser::FLAG_CJK_NGRAM
+     | Xapian::QueryParser::FLAG_DEFAULT);
+    }
+  // If the syntax is wrong (xxx AND xxx), Xapian throws this error.
+  // Try again without enabling any special syntax.
+  catch (Xapian::QueryParserError &e)
+    {
+      query = parser.parse_query
+        // Support CJK.
+        (term, Xapian::QueryParser::FLAG_CJK_NGRAM);
+    }
+  
+  Xapian::Enquire enquire (database);
+  enquire.set_query (query);
+
+  Xapian::MSet mset = enquire.get_mset (offset, page_size);
+  vector<string> result (0);
+  for (Xapian::MSetIterator it = mset.begin(); it != mset.end(); it++)
+    {
+      Xapian::Document doc = it.get_document();
+      string path = doc.get_value(DOC_FILEPATH);
+      // If the file doesn’t exists anymore, remove it.
+      struct stat st;
+      if (stat (path.c_str(), &st) == 0)
+        {
+          result.push_back (doc.get_value (DOC_FILEPATH));
+        }
+      else
+        {
+          database.delete_document (doc.get_docid());
+        }
+    }
+  return result;
+}
+
+/*** Module definition */
+
+/**** Convenient functions */
+
+static bool
+copy_string_contents
+(emacs_env *env, emacs_value value, char **buffer, size_t *size)
+{
+  ptrdiff_t buffer_size;
+  if (!env->copy_string_contents (env, value, NULL, &buffer_size))
+    return false;
+  assert (env->non_local_exit_check (env) == emacs_funcall_exit_return);
+  assert (buffer_size > 0);
+  *buffer = (char*) malloc ((size_t) buffer_size);
+  if (*buffer == NULL)
+    {
+      env->non_local_exit_signal (env, env->intern (env, "memory-full"),
+                                  env->intern (env, "nil"));
+      return false;
+    }
+  ptrdiff_t old_buffer_size = buffer_size;
+  if (!env->copy_string_contents (env, value, *buffer, &buffer_size))
+    {
+      free (*buffer);
+      *buffer = NULL;
+      return false;
+    }
+  assert (env->non_local_exit_check (env) == emacs_funcall_exit_return);
+  assert (buffer_size == old_buffer_size);
+  *size = (size_t) (buffer_size - 1);
+  return true;
+}
+
+static void
+bind_function (emacs_env *env, const char *name, emacs_value Sfun)
+{
+  emacs_value Qfset = env->intern (env, "fset");
+  emacs_value Qsym = env->intern (env, name);
+
+  emacs_value args[] = {Qsym, Sfun};
+  env->funcall (env, Qfset, 2, args);
+}
+
+static void
+provide (emacs_env *env, const char *feature)
+{
+  emacs_value Qfeat = env->intern (env, feature);
+  emacs_value Qprovide = env->intern (env, "provide");
+  emacs_value args[] = { Qfeat };
+
+  env->funcall (env, Qprovide, 1, args);
+}
+
+static emacs_value
+nil (emacs_env *env) {
+  return env->intern (env, "nil");
+}
+
+static emacs_value
+cons (emacs_env *env, emacs_value car, emacs_value cdr) {
+  emacs_value args[] = {car, cdr};
+  return env->funcall (env, env->intern(env, "cons"), 2, args);
+}
+
+static void
+signal (emacs_env *env, const char *name, const char *message)
+{
+  env->non_local_exit_signal
+    (env, env->intern (env, name),
+     cons (env, env->make_string (env, message, strlen (message)),
+           nil (env)));
+}
+
+static string
+copy_string (emacs_env *env, emacs_value value)
+{
+  char* char_buffer;
+  size_t size;
+  if (copy_string_contents (env, value, &char_buffer, &size))
+    {
+      string str = (string) char_buffer;
+      return str;
+    }
+  else
+    {
+      signal (env, "xeft-error",
+              "Error turning lisp string to C++ string");
+      return "";
+    }
+}
+
+void
+define_error
+(emacs_env *env, const char *name,
+ const char *description, const char *parent)
+{
+  emacs_value args[] = {
+    env->intern (env, name),
+    env->make_string (env, description, strlen (description)),
+    env->intern (env, parent)
+  };
+  env->funcall (env, env->intern (env, "define-error"), 3, args);
+}
+
+emacs_value
+file_name_absolute_p (emacs_env *env, emacs_value path)
+{
+  emacs_value args[] = {path};
+  return env->funcall
+    (env, env->intern (env, "file-name-absolute-p"), 1, args);
+}
+
+bool
+nilp (emacs_env *env, emacs_value val)
+{
+  return !env->is_not_nil (env, val);
+}
+
+static const char* xeft_reindex_file_doc =
+  "Refindex file at PATH with database at DBPATH"
+  "Both paths has to be absolute.  Normally, this function only"
+  "reindex a file if it has been modified since last indexed,"
+  "but if FORCE is non-nil, this function will always reindex."
+  "Return non-nil if actually reindexed the file, return nil if not."
+  ""
+  "LANG is the language used by the indexer, it tells Xapian how to"
+  "reduce words to stems and vice versa, e.g., apples <-> apple."
+  "A full list of possible languages can be found at"
+  "https://xapian.org/docs/apidoc/html/classXapian_1_1Stem.html.";
+  "By default, LANG is \"en\"."
+  ""
+  "\(PATH DBPATH &optional LANG FORCE)";
+
+static emacs_value
+Fxeft_reindex_file
+(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data) {
+  
+  emacs_value lisp_path = args[0];
+  emacs_value lisp_dbpath = args[1];
+
+  if (nilp (env, file_name_absolute_p (env, lisp_path)))
+    {
+      signal (env, "xeft-file-error", "PATH is not a absolute path");
+    }
+  if (nilp (env, file_name_absolute_p (env, lisp_dbpath)))
+    {
+      signal (env, "xeft-file-error", "DBPATH is not a absolute path");
+    }
+
+  emacs_value lisp_lang = nargs < 3 ? nil (env) : args[2];
+  emacs_value lisp_force = nargs < 4 ? nil (env) : args[3];
+  
+  string path = copy_string (env, lisp_path);
+  string dbpath = copy_string (env, lisp_dbpath);
+  bool force = !nilp (env, lisp_force);
+  string lang = nilp (env, lisp_lang) ?
+    "en" : copy_string (env, lisp_lang);
+
+  bool indexed;
+  try
+    {
+      indexed = reindex_file (path, dbpath, lang, force);
+    }
+  catch (xeft_cannot_open_file &e)
+    {
+      signal (env, "xeft-file-error", "Cannot open the file");
+    }
+  catch (Xapian::Error &e)
+    {
+      signal (env, "xeft-xapian-error", e.get_description().c_str());
+    }
+  catch (exception &e)
+    {
+      signal (env, "xeft-error", "Something went wrong");
+    }
+  
+  return indexed ? env->intern (env, "t") : nil (env);
+}
+
+static const char *xeft_query_term_doc =
+  "Query for TERM in database at DBPATH."
+  "Paging is supported by OFFSET and PAGE-SIZE. OFFSET specifies page"
+  "start, and PAGE-SIZE the size. For example, if a page is 10 entries,"
+  "OFFSET and PAGE-SIZE would be first 0 and 10, then 10 and 10, and"
+  "so on."
+  ""
+  "If a file in the result doesn't exist anymore, it is removed from"
+  "the database, and not included in the return value."
+  ""
+  "LANG is the language used by the indexer, it tells Xapian how to"
+  "reduce words to stems and vice versa, e.g., apples <-> apple."
+  "A full list of possible languages can be found at"
+  "https://xapian.org/docs/apidoc/html/classXapian_1_1Stem.html.";
+  "By default, LANG is \"en\"."
+  ""
+  "\(TERM DBPATH OFFSET PAGE-SIZE &optional LANG)";
+
+static emacs_value
+Fxeft_query_term
+(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data) {
+  emacs_value lisp_term = args[0];
+  emacs_value lisp_dbpath = args[1];
+  emacs_value lisp_offset = args[2];
+  emacs_value lisp_page_size = args[3];
+
+  if (nilp (env, file_name_absolute_p (env, lisp_dbpath)))
+    {
+      signal (env, "xeft-file-error", "DBPATH is not a absolute path");
+    }
+
+  string term = copy_string (env, lisp_term);
+  string dbpath = copy_string (env, lisp_dbpath);
+  int offset = env->extract_integer (env, lisp_offset);
+  int page_size = env->extract_integer (env, lisp_page_size);
+
+  vector<string> result;
+  try
+    {
+      result = query_term (term, dbpath, offset, page_size);
+    }
+  catch (Xapian::Error &e)
+    {
+      signal (env, "xeft-xapian-error", e.get_description().c_str());
+    }
+  catch (exception &e)
+    {
+      signal (env, "xeft-error", "Something went wrong");
+    }
+
+  vector<string>::iterator it;
+  emacs_value ret = nil (env);
+  for (it = result.begin(); it != result.end(); it++) {
+    ret = cons (env, env->make_string(env, it->c_str(),
+                                     strlen(it->c_str())),
+                ret);
+  }
+
+  return env->funcall (env, env->intern (env, "reverse"), 1, &ret);
+}
+
+int
+emacs_module_init (struct emacs_runtime *ert)
+{
+  emacs_env *env = ert->get_environment (ert);
+
+  define_error (env, "xeft-error", "Generic xeft error", "error");
+  define_error (env, "xeft-xapian-error", "Xapian error", "xeft-error");
+  define_error (env, "xeft-file-error", "Cannot open file", "xeft-error");
+
+  bind_function (env, "xeft-reindex-file",
+                 env->make_function
+                 (env, 2, 3, &Fxeft_reindex_file,
+                  xeft_reindex_file_doc, NULL));
+
+  bind_function (env, "xeft-query-term",
+                 env->make_function
+                 (env, 4, 4, &Fxeft_query_term,
+                  xeft_query_term_doc, NULL));
+
+  provide (env, "xeft-module");
+
+  /* Return 0 to indicate module loaded successfully.  */
+  return 0;
+}
diff --git a/xeft.el b/xeft.el
new file mode 100644
index 0000000000..4a78dedeb7
--- /dev/null
+++ b/xeft.el
@@ -0,0 +1,421 @@
+;;; xeft.el --- Yay note-taking      -*- lexical-binding: t; -*-
+
+;; Author: Yuan Fu <casouri@gmail.com>
+
+;;; This file is NOT part of GNU Emacs
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'cl-lib)
+(declare-function xeft-reindex-file nil (path dbpath lang force))
+(declare-function xeft-query-term nil (term dbpath offset page-size lang))
+
+;;; Customize
+
+(defgroup xeft nil
+  "Xeft note interface."
+  :group 'applications)
+
+(defcustom xeft-directory (expand-file-name "~/.deft")
+  "Directory in where notes are stored. Must be a full path."
+  :type 'directory)
+
+(defcustom xeft-database (expand-file-name "~/.deft/db")
+  "The path to the database."
+  :type 'directory)
+
+(defcustom xeft-find-file-hook nil
+  "Hook run when Xeft opens a file."
+  :type 'hook)
+
+(defface xeft-selection
+  '((t . (:inherit region :extend t)))
+  "Face for the current selected search result.")
+
+(defface xeft-inline-highlight
+  '((t . (:inherit underline :extend t)))
+  "Face for inline highlighting in Xeft buffer.")
+
+(defface xeft-preview-highlight
+  '((t . (:inherit highlight :extend t)))
+  "Face for highlighting in the preview buffer.")
+
+(defcustom xeft-load-file-hook nil
+  "Functions run before xeft loads a file into database."
+  :type 'hook)
+
+;;; Compile
+
+(defun xeft--compile-module ()
+  "Compile the dynamic module. Return non-nil if success."
+  ;; Just following vterm.el here.
+  (let* ((source-dir
+          (shell-quote-argument
+           (file-name-directory
+            (locate-library "xeft.el" t))))
+         (command (format "cd %s; make PREFIX=%s"
+                          source-dir
+                          (read-string "PREFIX: " "/usr/local")))
+         (buffer (get-buffer-create "*xeft compile*")))
+    (if (zerop (let ((inhibit-read-only t))
+                 (call-process "sh" nil buffer t "-c" command)))
+        (progn (message "Successfully compiled the module :-D") t)
+      (pop-to-buffer buffer)
+      (compilation-mode)
+      (message "Failed to compile the module")
+      nil)))
+
+;;; Helpers
+
+(defvar xeft--last-window-config nil
+  "Window configuration before Xeft starts.")
+
+(defun xeft--buffer ()
+  "Return the xeft buffer."
+  (get-buffer-create "*xeft*"))
+
+(defun xeft--work-buffer ()
+  "Return the work buffer for Xeft. Used for holding file contents."
+  (get-buffer-create " *xeft work*"))
+
+(defun xeft--after-save-hook ()
+  "Reindex the file."
+  (xeft-reindex-file (buffer-file-name) xeft-database))
+
+(defvar xeft-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "RET") #'xeft-create-note)
+    (define-key map (kbd "C-c C-g") #'xeft-refresh-full)
+    (define-key map (kbd "C-n") #'xeft-next)
+    (define-key map (kbd "C-p") #'xeft-previous)
+    map)
+  "Mode map for `xeft-mode'.")
+
+(define-derived-mode xeft-mode fundamental-mode
+  "Xeft" "Search for notes and display summaries."
+  (let ((inhibit-read-only t))
+    ;; Reindex all files.
+    (dolist (file (xeft--file-list))
+      (xeft-reindex-file file xeft-database))
+    (visual-line-mode)
+    (setq default-directory xeft-directory
+          xeft--last-window-config (current-window-configuration))
+    (add-hook 'after-change-functions
+              (lambda (&rest _) (xeft-refresh)) 0 t)
+    (add-hook 'window-size-change-functions
+              (lambda (&rest _) (xeft-refresh))0 t)
+    (add-hook 'kill-buffer-hook
+              (lambda ()
+                (when xeft--last-window-config
+                  (set-window-configuration xeft--last-window-config)))
+              0 t)
+    (erase-buffer)
+    (insert "\n\nInsert search phrase and press RET to search.")
+    (goto-char (point-min))))
+
+
+;;; Userland
+
+;;;###autoload
+(defun xeft ()
+  "Start Xeft."
+  (interactive)
+  (unless (require 'xeft-module nil t)
+    (when (y-or-n-p
+           "Xeft needs the dynamic module to work, compile it now? ")
+      (when (xeft--compile-module)
+        (require 'xeft-module))))
+  (setq xeft--last-window-config (current-window-configuration))
+  (switch-to-buffer (xeft--buffer))
+  (when (not (derived-mode-p 'xeft-mode))
+    (xeft-mode)))
+
+(defun xeft-create-note ()
+  "Create a new note with the current search phrase as the title."
+  (interactive)
+  (let* ((search-phrase (xeft--get-search-phrase))
+         (file-path (expand-file-name (concat search-phrase ".txt")
+                                      xeft-directory))
+         (exists-p (file-exists-p file-path)))
+    ;; If there is no match, create the file without confirmation,
+    ;; otherwise prompt for confirmation. NOTE: this is not DRY, but
+    ;; should be ok.
+    (when (or (search-forward "Press RET to create a new note" nil t)
+              (y-or-n-p (format "Create file `%s'.txt? " search-phrase)))
+      (find-file file-path)
+      (unless exists-p
+        (insert search-phrase "\n\n")
+        (save-buffer))
+      (run-hooks 'xeft-find-file-hook))))
+
+(defvar-local xeft--select-overlay nil
+  "Overlay used for highlighting selected search result.")
+
+(defun xeft--highlight-file-at-point ()
+  "Activate (highlight) the file excerpt button at point."
+  (when-let ((button (button-at (point))))
+    ;; Create the overlay if it doesn't exist yet.
+    (when (null xeft--select-overlay)
+      (setq xeft--select-overlay (make-overlay (button-start button)
+                                               (button-end button)))
+      (overlay-put xeft--select-overlay 'evaporate t)
+      (overlay-put xeft--select-overlay 'face 'xeft-selection))
+    ;; Move the overlay over the file.
+    (move-overlay xeft--select-overlay
+                  (button-start button) (button-end button))))
+
+(defun xeft-next ()
+  "Move to next file excerpt."
+  (interactive)
+  (when (forward-button 1 nil nil t)
+    (xeft--highlight-file-at-point)))
+
+(defun xeft-previous ()
+  "Move to previous file excerpt."
+  (interactive)
+  (if (backward-button 1 nil nil t)
+      (xeft--highlight-file-at-point)
+    ;; Go to the end of the search phrase.
+    (goto-char (point-min))
+    (end-of-line)))
+
+;;; Draw
+
+(defvar xeft--preview-window nil
+  "Xeft shows file previews in this window.")
+
+(defvar xeft--cache nil
+  "An alist of (filename . file content).")
+
+(defun xeft--cached-insert (file)
+  "Insert the content of FILE, and cache it."
+  (if-let ((content (alist-get file xeft--cache nil nil #'equal)))
+      (insert content)
+    (insert-file-contents file)
+    (setf (alist-get file xeft--cache nil nil #'equal)
+          (buffer-string))))
+
+(defun xeft--get-search-phrase ()
+  "Return the search phrase. Assumes current buffer is a xeft buffer."
+  (save-excursion
+    (goto-char (point-min))
+    (string-trim
+     (buffer-substring-no-properties (point) (line-end-position)))))
+
+(defun xeft--find-file-at-point ()
+  "View file at point."
+  (interactive)
+  (find-file (button-get (button-at (point)) 'path))
+  (add-hook 'after-save-hook #'xeft--after-save-hook 0 t))
+
+(defun xeft--preview-file (file &optional select)
+  "View FILE in another window.
+If SELECT is non-nil, select the buffer after displaying it."
+  (interactive)
+  (let* ((buffer (find-file-noselect file))
+         (search-phrase (xeft--get-search-phrase))
+         (keyword-list (split-string search-phrase)))
+    (if (and (window-live-p xeft--preview-window)
+             (not (eq xeft--preview-window (selected-window))))
+        (with-selected-window xeft--preview-window
+          (switch-to-buffer buffer))
+      (setq xeft--preview-window
+            (display-buffer
+             buffer '((display-buffer-use-some-window
+                       display-buffer-in-direction
+                       display-buffer-pop-up-window)
+                      . ((inhibit-same-window . t)
+                         (direction . right)
+                         (window-width
+                          . (lambda (win)
+                              (let ((width (window-width)))
+                                (when (< width 50)
+                                  (window-resize
+                                   win (- 50 width) t))))))))))
+    (if select (select-window xeft--preview-window))
+    (with-current-buffer buffer
+      (xeft--highlight-matched keyword-list)
+      (run-hooks 'xeft-find-file-hook))))
+
+(define-button-type 'xeft-excerpt
+  'action (lambda (button)
+            ;; If the file is no already highlighted, highlight it
+            ;; first.
+            (when (not (and xeft--select-overlay
+                            (overlay-buffer xeft--select-overlay)
+                            (<= (overlay-start xeft--select-overlay)
+                                (button-start button)
+                                (overlay-end xeft--select-overlay))))
+              (goto-char (button-start button))
+              (xeft--highlight-file-at-point))
+            (xeft--preview-file (button-get button 'path)))
+  'keymap (let ((map (make-sparse-keymap)))
+            (set-keymap-parent map button-map)
+            (define-key map (kbd "RET") #'xeft--find-file-at-point)
+            (define-key map (kbd "SPC") #'push-button)
+            map)
+  'help-echo "Open this file"
+  'follow-link t
+  'face 'default
+  'mouse-face 'xeft-selection)
+
+(defun xeft--highlight-search-phrase ()
+  "Highlight search phrases in buffer."
+  (let ((keyword-list (split-string (xeft--get-search-phrase)))
+        (inhibit-read-only t))
+    (dolist (keyword keyword-list)
+      (goto-char (point-min))
+      (forward-line 2)
+      ;; We use overlay because overlay allows face composition.
+      ;; So we can have bold + underline.
+      (while (search-forward keyword nil t)
+        (let ((ov (make-overlay (match-beginning 0)
+                                (match-end 0))))
+          (overlay-put ov 'face 'xeft-inline-highlight)
+          (overlay-put ov 'xeft-highlight t)
+          (overlay-put ov 'evaporate t))))))
+
+(defun xeft--insert-file-excerpt (file search-phrase)
+  "Insert an excerpt for FILE at point.
+This excerpt contains note title and content excerpt and is
+clickable. FILE should be an absolute path. SEARCH-PHRASE is the
+search phrase the user typed."
+  (let ((excerpt-len (floor (* 2.7 (1- (window-width)))))
+        (last-search-term
+         (car (last (split-string search-phrase))))
+        title excerpt)
+    (with-current-buffer (xeft--work-buffer)
+      (widen)
+      (erase-buffer)
+      (xeft--cached-insert file)
+      (goto-char (point-min))
+      (search-forward "#+TITLE: " (line-end-position) t)
+      (setq title (buffer-substring-no-properties
+                   (point) (line-end-position)))
+      (when (eq title "") (setq title "no title"))
+      (forward-line)
+      (narrow-to-region (point) (point-max))
+      ;; Grab excerpt.
+      (setq excerpt (string-trim
+                     (replace-regexp-in-string
+                      "[[:space:]]+"
+                      " "
+                      (if (and last-search-term
+                               (search-forward last-search-term nil t))
+                          (buffer-substring-no-properties
+                           (max (- (point) (/ excerpt-len 2))
+                                (point-min))
+                           (min (+ (point) (/ excerpt-len 2))
+                                (point-max)))
+                        (buffer-substring-no-properties
+                         (point)
+                         (min (+ (point) excerpt-len)
+                              (point-max))))))))
+    ;; Now we insert the excerpt
+    (let ((start (point)))
+      (insert (propertize title 'face '(:weight bold))
+              "\n"
+              (propertize excerpt 'face '(:weight light))
+              "\n\n")
+      ;; If we use overlay (with `make-button'), the button's face
+      ;; will override the bold and light face we specified above.
+      (make-text-button start (- (point) 2)
+                        :type 'xeft-excerpt
+                        'path file))))
+
+;;; Refresh and search
+
+(defun xeft-refresh-full ()
+  "Refresh and display _all_ results."
+  (interactive)
+  (xeft-refresh t))
+
+(defun xeft--file-list ()
+  "Return a list of all files in ‘xeft-directory’."
+  (cl-remove-if-not
+   (lambda (file)
+     (and (file-regular-p file)
+          (not (string-prefix-p
+                "." (file-name-base file)))))
+   (directory-files xeft-directory t nil t)))
+
+(defun xeft-refresh (&optional full)
+  "Search for notes and display their summaries.
+By default, only display the first 50 results. If FULL is
+non-nil, display all results."
+  (interactive)
+  (let ((search-phrase (xeft--get-search-phrase)))
+    (when (derived-mode-p 'xeft-mode)
+      (let* ((phrase-empty (equal search-phrase ""))
+             (file-list
+              (if phrase-empty
+                  (cl-sort (xeft--file-list) #'file-newer-than-file-p)
+                (xeft-query-term search-phrase xeft-database
+                                 0 (if full 2147483647 50)))))
+        (when (and (null full) (> (length file-list) 50))
+          (setq file-list (cl-subseq file-list 0 50)))
+        (let ((inhibit-read-only t)
+              (inhibit-modification-hooks t)
+              (orig-point (point)))
+          ;; Actually insert the new content.
+          (goto-char (point-min))
+          (forward-line 2)
+          (delete-region (point) (point-max))
+          (when (while-no-input
+                  (let ((start (point)))
+                    (insert (if file-list
+                                (with-temp-buffer
+                                  (dolist (file file-list)
+                                    (xeft--insert-file-excerpt
+                                     file search-phrase))
+                                  (buffer-string))
+                              ;; NOTE: this string is referred in
+                              ;; ‘xeft-create-note’.
+                              "Press RET to create a new note"))
+                    ;; If we use (- start 2), emacs-rime cannot work.
+                    (put-text-property (- start 1) (point)
+                                       'read-only t))
+                  (xeft--highlight-search-phrase)
+                  (set-buffer-modified-p nil)
+                  ;; Save excursion wouldn’t work since we erased the
+                  ;; buffer and re-inserted contents.
+                  (goto-char orig-point)
+                  ;; Re-apply highlight.
+                  (xeft--highlight-file-at-point))
+            (goto-char orig-point)))))))
+
+;;; Highlight matched phrases
+
+(defun xeft--highlight-matched (keyword-list)
+  "Highlight keywords in KEYWORD-LIST in the current buffer."
+  (save-excursion
+    ;; Add highlight overlays.
+    (dolist (keyword keyword-list)
+      (goto-char (point-min))
+      (while (search-forward keyword nil t)
+        (let ((ov (make-overlay (match-beginning 0)
+                                (match-end 0))))
+          (overlay-put ov 'face 'xeft-preview-highlight)
+          (overlay-put ov 'xeft-highlight t))))
+    ;; Add cleanup hook.
+    (add-hook 'window-selection-change-functions
+              #'xeft--cleanup-highlight
+              0 t)))
+
+(defun xeft--cleanup-highlight (window)
+  "Cleanup highlights in WINDOW."
+  (when (eq window (selected-window))
+    (let ((ov-list (overlays-in (point-min)
+                                (point-max))))
+      (dolist (ov ov-list)
+        (when (overlay-get ov 'xeft-highlight)
+          (delete-overlay ov))))
+    (remove-hook 'window-selection-change-functions
+                 #'xeft--cleanup-highlight
+                 t)))
+
+(provide 'xeft)
+
+;;; xeft.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]