From 0b399ed1c7038b1c4ae5c5dabe365138dd483c69 Mon Sep 17 00:00:00 2001 From: Denis Zubarev Date: Tue, 3 Oct 2023 22:30:27 +0300 Subject: [PATCH] speed up auto-completion when using company If the point is on dot (name.| or name.var|) then synchronously collect candidates. Otherwise use cached data. When cache is empty (first time invocation), it returns only common SQL words and schedules the cache update. Delayed cache update is done via run-with-idle-timer. Also cache is updated when it is older than ejc-company-cache-update-ivl-secs (60 by default). It allows to reduce completion time from 0.4 s to 0.0011 for cases like (SE|) --- ejc-company.el | 127 ++++++++++++++++++++++++++++++--------- ejc-completion-common.el | 11 +--- 2 files changed, 101 insertions(+), 37 deletions(-) diff --git a/ejc-company.el b/ejc-company.el index 527379a..749c237 100644 --- a/ejc-company.el +++ b/ejc-company.el @@ -1,4 +1,4 @@ -;;; ejc-company.el -- SQL completitions at point by company-mode (the part of ejc-sql). +;;; ejc-company.el -- SQL completitions at point by company-mode (the part of ejc-sql). -*- lexical-binding: t -*- ;;; Copyright © 2020 - Kostafey @@ -31,6 +31,25 @@ (require 'company) (require 'ejc-completion-common) +(defcustom ejc-company-cache-update-ivl-secs 60 + "Specify how often to update cached candidates in seconds. +If set to 1.0e+INF, do not update cache after initialization." + :type 'integer :group 'ejc-sql) + +(defcustom ejc-company-idle-timer-secs 1 + "Collect candidates after specified amount of idleness in seconds." + :type 'integer :group 'ejc-sql) + +(defvar-local ejc-company--candidates nil + "Cached candidates.") + +(defvar-local ejc-company--cache-update-ts nil + "Last timestamp of cache update.") + +(defvar-local ejc-company--cache-update-scheduled nil + "Whether `ejc-company--cache-candidates' is already scheduled with `run-with-idle-timer'.") + + (defun ejc-company-make-candidate (candidate) (let ((text (car candidate)) (meta (cadr candidate))) @@ -40,35 +59,85 @@ (-map (lambda (k) (list k meta)) candidates)) +(defun ejc-company--collect-all-candidates (&optional on-point) + (append + (ejc-append-without-duplicates + (ejc-company-add-meta + "ansi sql" (ejc-get-ansi-sql-words)) + (ejc-company-add-meta + "keyword" (ejc-get-keywords)) + 'car :right) + (ejc-company-add-meta + "owner" (ejc-owners-candidates)) + (ejc-company-add-meta + "table" (ejc-tables-candidates)) + (ejc-company-add-meta + "view" (ejc-views-candidates)) + (when (not on-point) + (ejc-company-add-meta + "package" (ejc-packages-candidates))) + (when on-point + (ejc-company-add-meta + "column" (ejc-colomns-candidates))))) + +(defun ejc-company-make-candidates (prefix items) + "Filter `ITEMS' that are not started with `PREFIX' and prepare them for company." + (mapcar #'ejc-company-make-candidate + (if (string= "" prefix) + items + (cl-remove-if-not + (lambda (c) (string-prefix-p prefix (car c) t)) + items)))) + +(defun ejc-company--cache-candidates (buffer) + "Collect candidates for `BUFFER' and put them into a buffer-local variable." + (when (buffer-live-p buffer) + (with-current-buffer buffer + (save-excursion + ;; we have to collect keywords, table/view names etc. + ;; Since this function is invoked by timer we can't guarantee that point is not on a dot (e.g. var.|). + ;; All collect functions will check buffer position and behave differently if point is on the dot. + ;; So we should make sure not to be on the dot. + ;; The simplest is just go to the beginning of a buffer + (goto-char (point-min)) + (condition-case err-cons + (setq ejc-company--candidates (ejc-company--collect-all-candidates)) + (error (message (cadr err-cons)))) + (setq ejc-company--cache-update-ts (float-time) + ejc-company--cache-update-scheduled nil))))) + +(defun ejc-company--schedule-cache-update () + "Schedule cache update if cache is empty or it was updated too long ago." + (when (and (not ejc-company--cache-update-scheduled) + (or (not ejc-company--candidates) + (not ejc-company--cache-update-ts) + (> (- (float-time) ejc-company--cache-update-ts) + ejc-company-cache-update-ivl-secs))) + (run-with-idle-timer ejc-company-idle-timer-secs + nil + #'ejc-company--cache-candidates (current-buffer)) + (setq ejc-company--cache-update-scheduled t))) + (defun ejc-company-candidates (prefix) - (let* ((prefix-1 (ejc-get-prefix-word)) - (prefix-2 (save-excursion - (search-backward "." nil t) - (ejc-get-prefix-word))) - (res)) - (dolist (item - (cl-remove-if-not - (lambda (c) (string-prefix-p prefix (car c) t)) - (append - (ejc-append-without-duplicates - (ejc-company-add-meta - "ansi sql" (ejc-get-ansi-sql-words)) - (ejc-company-add-meta - "keyword" (ejc-get-keywords)) - 'car :right) - (ejc-company-add-meta - "owner" (ejc-owners-candidates)) - (ejc-company-add-meta - "table" (ejc-tables-candidates)) - (ejc-company-add-meta - "view" (ejc-views-candidates)) - (if (not prefix-1) - (ejc-company-add-meta - "package" (ejc-packages-candidates))) - (ejc-company-add-meta - "column" (ejc-colomns-candidates))))) - (push (ejc-company-make-candidate item) res)) - res)) + "If the point is on dot (name.| or name.var|) then synchronously collect candidates. +Otherwise use cached data. When cache is empty (first time invocation), +it returns only common SQL words and schedules the cache update. +`PREFIX' is used for filtering candidates." + + + (let* ((on-point (ejc-get-prefix-word))) + (if on-point + (ejc-company-make-candidates prefix (ejc-company--collect-all-candidates t)) + + (ejc-company--schedule-cache-update) + (if ejc-company--candidates + (ejc-company-make-candidates prefix ejc-company--candidates) + + ;; first time invocation, return only sql words + (ejc-company-make-candidates prefix + (setq ejc-company--candidates + (ejc-company-add-meta + "ansi sql" (ejc-get-ansi-sql-words)))))))) (defun ejc-company-annotation (candidate) (format " %s" (get-text-property 0 'meta candidate))) diff --git a/ejc-completion-common.el b/ejc-completion-common.el index 980b2ea..4d6da72 100644 --- a/ejc-completion-common.el +++ b/ejc-completion-common.el @@ -48,14 +48,9 @@ Uppercase by default, set to nil to use downcase candidates." (defun ejc-return-point () "Return point position if point (cursor) is located next to dot char (.#)" - (let ((curr-char (buffer-substring - (save-excursion - (left-char 1) - (point)) - (point)))) - (if (equal curr-char ".") - (point) - nil))) + (if (= ?. (or (char-before) 0)) + (point) + nil)) (defun ejc-get-prefix-word () "Return the word preceding dot before the typing."