branch: externals/xeft commit afc8a69a526ab7662bf68eb87ee76d5547116bb0 Author: Yuan Fu <caso...@gmail.com> Commit: Yuan Fu <caso...@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 <caso...@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