company-fuzzy.el
1 |
|
2 |
|
3 |
;; Copyright (C) 2019-2024 Shen, Jen-Chieh |
4 |
;; Created date 2019-08-01 16:54:34 |
5 |
;; FIXED by Maarten Vangeneugden at 2024-09-15 |
6 |
|
7 |
;; Author: Shen, Jen-Chieh <jcs090218@gmail.com> |
8 |
;; URL: https://github.com/jcs-elpa/company-fuzzy |
9 |
;; Version: 1.4.0 |
10 |
;; Package-Requires: ((emacs "26.1") (company "0.8.12") (s "1.12.0") (ht "2.0")) |
11 |
;; Keywords: matching auto-complete complete fuzzy |
12 |
|
13 |
;; This file is NOT part of GNU Emacs. |
14 |
|
15 |
;; This program is free software; you can redistribute it and/or modify |
16 |
;; it under the terms of the GNU General Public License as published by |
17 |
;; the Free Software Foundation, either version 3 of the License, or |
18 |
;; (at your option) any later version. |
19 |
|
20 |
;; This program is distributed in the hope that it will be useful, |
21 |
;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
22 |
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
23 |
;; GNU General Public License for more details. |
24 |
|
25 |
;; You should have received a copy of the GNU General Public License |
26 |
;; along with this program. If not, see <https://www.gnu.org/licenses/>. |
27 |
|
28 |
;;; Commentary: |
29 |
;; |
30 |
;; Fuzzy matching for `company-mode'. |
31 |
;; |
32 |
|
33 |
;;; Code: |
34 |
|
35 |
(require 'cl-lib) |
36 |
(require 'ffap) |
37 |
(require 'subr-x) |
38 |
|
39 |
(require 'company) |
40 |
(require 'ht) |
41 |
(require 's) |
42 |
|
43 |
(defgroup company-fuzzy nil |
44 |
"Fuzzy matching for `company-mode'." |
45 |
:prefix "company-fuzzy-" |
46 |
:group 'company |
47 |
:link '(url-link :tag "Repository" "https://github.com/jcs-elpa/company-fuzzy")) |
48 |
|
49 |
(defcustom company-fuzzy-sorting-backend 'alphabetic |
50 |
"Type for sorting/scoring backend." |
51 |
:type '(choice (const :tag "none" none) |
52 |
(const :tag "alphabetic" alphabetic) |
53 |
(const :tag "flex" flex) |
54 |
(const :tag "flx" flx) |
55 |
(const :tag "flx-rs" flx-rs) |
56 |
(const :tag "flxy" flxy) |
57 |
(const :tag "fuz-skim" fuz-skim) |
58 |
(const :tag "fuz-clangd" fuz-clangd) |
59 |
(const :tag "fuz-bin-skim" fuz-bin-skim) |
60 |
(const :tag "fuz-bin-clangd" fuz-bin-clangd) |
61 |
(const :tag "liquidmetal" liquidmetal) |
62 |
(const :tag "sublime-fuzzy" sublime-fuzzy)) |
63 |
:group 'company-fuzzy) |
64 |
|
65 |
(defcustom company-fuzzy-prefix-on-top t |
66 |
"Have the matching prefix on top." |
67 |
:type 'boolean |
68 |
:group 'company-fuzzy) |
69 |
|
70 |
(defcustom company-fuzzy-sorting-function nil |
71 |
"Function that gives all candidates and let you do your own sorting." |
72 |
:type '(choice (const :tag "None" nil) |
73 |
function) |
74 |
:group 'company-fuzzy) |
75 |
|
76 |
(defcustom company-fuzzy-sorting-score-function nil |
77 |
"Function that gives candidates with same score and let you do your own sorting." |
78 |
:type '(choice (const :tag "None" nil) |
79 |
function) |
80 |
:group 'company-fuzzy) |
81 |
|
82 |
(defcustom company-fuzzy-show-annotation t |
83 |
"Show annotation from source." |
84 |
:type 'boolean |
85 |
:group 'company-fuzzy) |
86 |
|
87 |
(defcustom company-fuzzy-annotation-format " <%s>" |
88 |
"Annotation string format." |
89 |
:type 'string |
90 |
:group 'company-fuzzy) |
91 |
|
92 |
(defcustom company-fuzzy-passthrough-backends nil |
93 |
"List of backends that already are fuzzy, so no filtering of candidates is done." |
94 |
:type 'list |
95 |
:group 'company-fuzzy) |
96 |
|
97 |
(defcustom company-fuzzy-trigger-symbols '("." "->") |
98 |
"List of symbols that allow trigger company when there is no prefix." |
99 |
:type 'list |
100 |
:group 'company-fuzzy) |
101 |
|
102 |
(defcustom company-fuzzy-completion-separator "[ \t\r\n]\\|\\_<\\|\\_>" |
103 |
"Use to identify the completion unit." |
104 |
:type 'string |
105 |
:group 'company-fuzzy) |
106 |
|
107 |
(defcustom company-fuzzy-reset-selection nil |
108 |
"If non-nil, reset the selection to default." |
109 |
:type 'boolean |
110 |
:group 'company-fuzzy) |
111 |
|
112 |
(defface company-fuzzy-annotation-face |
113 |
'((t (:inherit company-tooltip-annotation))) |
114 |
"Face for annotation." |
115 |
:group 'company-fuzzy) |
116 |
|
117 |
(defvar-local company-fuzzy--prefix "" |
118 |
"Generic prefix.") |
119 |
|
120 |
(defvar-local company-fuzzy--prefix-first "" |
121 |
"Store generic prefix's first character for caching.") |
122 |
|
123 |
(defvar-local company-fuzzy--backends nil |
124 |
"Company fuzzy backends we are going to use.") |
125 |
|
126 |
(defvar-local company-fuzzy--recorded-backends nil |
127 |
"Record down company local backends in current buffer.") |
128 |
|
129 |
(defvar-local company-fuzzy--is-trigger-prefix-p nil |
130 |
"Flag to see if currently completion having a valid prefix.") |
131 |
|
132 |
(defvar-local company-fuzzy--prefixes (make-hash-table :test 'equal) |
133 |
"Map for each backend's prefix.") |
134 |
|
135 |
(defvar-local company-fuzzy--candidates (make-hash-table :test 'equal) |
136 |
"Map for each bakend's candidates.") |
137 |
|
138 |
;; |
139 |
;; (@* "External" ) |
140 |
;; |
141 |
|
142 |
(declare-function flex-score "ext:flex.el") |
143 |
(declare-function flx-score "ext:flx.el") |
144 |
(declare-function flx-rs-score "ext:flx-rs.el") |
145 |
(declare-function flx-rs-load-dyn "ext:flx-rs.el") |
146 |
|
147 |
(declare-function flxy-score "ext:flxy.el") |
148 |
(declare-function flxy-load-dyn "ext:flxy.el") |
149 |
|
150 |
(declare-function fuz-calc-score-skim "ext:fuz.el") |
151 |
(declare-function fuz-calc-score-clangd "ext:fuz.el") |
152 |
(declare-function fuz-build-and-load-dymod "ext:fuz.el") |
153 |
|
154 |
(declare-function fuz-bin-score-skim "ext:fuz-bin.el") |
155 |
(declare-function fuz-bin-score-clangd "ext:fuz-bin.el") |
156 |
(declare-function fuz-bin-load-dyn "ext:fuz-bin.el") |
157 |
|
158 |
(declare-function liquidmetal-score "ext:liquidmetal.el") |
159 |
|
160 |
(declare-function sublime-fuzzy-score "ext:sublime-fuzzy.el") |
161 |
(declare-function sublime-fuzzy-load-dyn "ext:sublime-fuzzy.el") |
162 |
|
163 |
(declare-function company-emmet--prefix "ext:company-emmet.el") |
164 |
|
165 |
;; |
166 |
;; (@* "Mode" ) |
167 |
;; |
168 |
|
169 |
(defun company-fuzzy--init () |
170 |
"Initialize all sorting backends." |
171 |
(cl-case company-fuzzy-sorting-backend |
172 |
(`flex (require 'flex)) |
173 |
(`flx (require 'flx)) |
174 |
(`flx-rs (require 'flx-rs) (flx-rs-load-dyn)) |
175 |
(`flxy (require 'flxy) (flxy-load-dyn)) |
176 |
((or fuz-skim fuz-clangd) |
177 |
(require 'fuz) |
178 |
(unless (require 'fuz-core nil t) (fuz-build-and-load-dymod))) |
179 |
((or fuz-bin-skim fuz-bin-clangd) |
180 |
(require 'fuz-bin) (fuz-bin-load-dyn)) |
181 |
(`liquidmetal (require 'liquidmetal)) |
182 |
(`sublime-fuzzy (require 'sublime-fuzzy) (sublime-fuzzy-load-dyn)))) |
183 |
|
184 |
(defun company-fuzzy--enable () |
185 |
"Record down all other backend to `company-fuzzy--backends'." |
186 |
(company-fuzzy--init) |
187 |
;; XXX Don't know why, but you need to clear it first to make local |
188 |
;; variables work! |
189 |
(ht-clear company-fuzzy--prefixes) |
190 |
(ht-clear company-fuzzy--candidates) |
191 |
(unless company-fuzzy--recorded-backends |
192 |
(setq company-fuzzy--recorded-backends company-backends |
193 |
company-fuzzy--backends (company-fuzzy--normalize-backend-list company-fuzzy--recorded-backends)) |
194 |
(setq-local company-backends '(company-fuzzy-all-other-backends)) |
195 |
(setq-local company-transformers (append company-transformers '(company-fuzzy--sort-candidates))) |
196 |
(advice-add 'company--insert-candidate :before #'company-fuzzy--insert-candidate) |
197 |
(advice-add 'company-yasnippet--completions-for-prefix :around #'company-fuzzy-yasnippet--completions-for-prefix)) |
198 |
(add-hook 'lsp-completion-mode-hook #'company-fuzzy--lsp-after-enabled nil t) |
199 |
(add-hook 'eglot-managed-mode-hook #'company-fuzzy--lsp-after-enabled nil t)) |
200 |
|
201 |
(defun company-fuzzy--disable () |
202 |
"Revert all other backend back to `company-backends'." |
203 |
(when company-fuzzy--recorded-backends |
204 |
(setq-local company-backends company-fuzzy--recorded-backends) |
205 |
(setq-local company-transformers (delq 'company-fuzzy--sort-candidates company-transformers)) |
206 |
(setq company-fuzzy--recorded-backends nil |
207 |
company-fuzzy--backends nil) |
208 |
(advice-remove 'company--insert-candidate #'company-fuzzy--insert-candidate) |
209 |
(advice-remove 'company-yasnippet--completions-for-prefix #'company-fuzzy-yasnippet--completions-for-prefix)) |
210 |
(remove-hook 'lsp-completion-mode-hook #'company-fuzzy--lsp-after-enabled t) |
211 |
(remove-hook 'eglot-managed-mode-hook #'company-fuzzy--lsp-after-enabled t)) |
212 |
|
213 |
;;;###autoload |
214 |
(define-minor-mode company-fuzzy-mode |
215 |
"Minor mode `company-fuzzy-mode'." |
216 |
:lighter " ComFuz" |
217 |
:group company-fuzzy |
218 |
(if company-fuzzy-mode (company-fuzzy--enable) (company-fuzzy--disable))) |
219 |
|
220 |
(defun company-fuzzy-turn-on-company-fuzzy-mode () |
221 |
"Turn on the `company-fuzzy-mode'." |
222 |
(company-fuzzy-mode 1)) |
223 |
|
224 |
;;;###autoload |
225 |
(define-globalized-minor-mode global-company-fuzzy-mode |
226 |
company-fuzzy-mode company-fuzzy-turn-on-company-fuzzy-mode |
227 |
:group 'company-fuzzy |
228 |
:require 'company-fuzzy) |
229 |
|
230 |
;; |
231 |
;; (@* "Utilies" ) |
232 |
;; |
233 |
|
234 |
(defun company-fuzzy--valid-candidates-p (candidates) |
235 |
"Return non-nil if CANDIDATES is list of valid candidates." |
236 |
(ignore-errors (stringp (nth 0 candidates)))) |
237 |
|
238 |
(defun company-fuzzy--async-candidates-p (candidates) |
239 |
"Return non-nil if CANDIDATES is in async format." |
240 |
(when (consp candidates) |
241 |
(and (eq (car candidates) :async) (functionp (cdr candidates))))) |
242 |
|
243 |
(defun company-fuzzy--symbol-start () |
244 |
"Return symbol start point from current cursor position." |
245 |
(ignore-errors |
246 |
(save-excursion |
247 |
(forward-char -1) |
248 |
(re-search-backward company-fuzzy-completion-separator) |
249 |
(point)))) |
250 |
|
251 |
(defun company-fuzzy--furthest-prefix () |
252 |
"Return the possible furthest (greatest length) prefix." |
253 |
(ht-clear company-fuzzy--prefixes) |
254 |
(let ((final-len 0) final-prefix) |
255 |
(dolist (backend company-fuzzy--backends) |
256 |
(when-let ((prefix (ignore-errors (funcall backend 'prefix)))) |
257 |
(ht-set company-fuzzy--prefixes backend prefix) |
258 |
(when-let* ((len (length prefix)) |
259 |
((< final-len len))) |
260 |
(setq final-prefix prefix |
261 |
final-len len)))) |
262 |
final-prefix)) |
263 |
|
264 |
(defun company-fuzzy--generic-prefix () |
265 |
"Return the most generic prefix." |
266 |
(let ((start (company-fuzzy--symbol-start))) |
267 |
(ignore-errors |
268 |
(string-trim (buffer-substring-no-properties (or start (point-min)) (point)))))) |
269 |
|
270 |
(defun company-fuzzy--trigger-prefix-p () |
271 |
"Check if current prefix a trigger prefix." |
272 |
(member company-fuzzy--prefix company-fuzzy-trigger-symbols)) |
273 |
|
274 |
(defun company-fuzzy--string-match (regexp string &optional start) |
275 |
"Safe way to execute function `string-match'. |
276 |
See function `string-match' for arguments REGEXP, STRING and START." |
277 |
(or (ignore-errors (string-match regexp string start)) |
278 |
(ignore-errors (string-match (regexp-quote regexp) string start)))) |
279 |
|
280 |
(defun company-fuzzy--string-match-p (regexp string &optional start) |
281 |
"Safe way to execute function `string-match-p'. |
282 |
See function `string-match-p' for arguments REGEXP, STRING and START." |
283 |
(or (ignore-errors (string-match-p regexp string start)) |
284 |
(ignore-errors (string-match-p (regexp-quote regexp) string start)))) |
285 |
|
286 |
(defun company-fuzzy--string-prefix-p (prefix string &optional ignore-case) |
287 |
"Safe way to execute function `string-prefix-p'. |
288 |
See function `string-prefix-p' for arguments PREFIX, STRING and IGNORE-CASE." |
289 |
(ignore-errors (string-prefix-p prefix string ignore-case))) |
290 |
|
291 |
(defun company-fuzzy--normalize-backend-list (backends) |
292 |
"Normalize all BACKENDS as list." |
293 |
(let (result-lst) |
294 |
(dolist (backend backends) |
295 |
(if (listp backend) |
296 |
(let ((index 0)) |
297 |
(dolist (back backend) |
298 |
(when (company-fuzzy--string-prefix-p "company-" (symbol-name back)) |
299 |
(push (nth index backend) result-lst)) |
300 |
(setq index (1+ index)))) |
301 |
(push backend result-lst))) |
302 |
(setq result-lst (reverse result-lst)) |
303 |
(cl-remove-duplicates result-lst))) |
304 |
|
305 |
(defun company-fuzzy--get-backend-by-candidate (candidate) |
306 |
"Return the backend symbol by using CANDIDATE as search index." |
307 |
(let ((match (ht-find (lambda (_backend cands) |
308 |
(member candidate cands)) |
309 |
company-fuzzy--candidates))) |
310 |
(car match))) |
311 |
|
312 |
(defun company-fuzzy--call-backend (backend command key) |
313 |
"Safely call BACKEND by COMMAND and KEY." |
314 |
(ignore-errors (funcall backend command key))) |
315 |
|
316 |
(defun company-fuzzy--backend-command (candidate command) |
317 |
"Find the backend from the CANDIDATE then call the COMMAND." |
318 |
(unless (string-empty-p candidate) |
319 |
(when-let ((backend (company-fuzzy--get-backend-by-candidate candidate))) |
320 |
(company-fuzzy--call-backend backend command candidate)))) |
321 |
|
322 |
;; |
323 |
;; (@* "Annotation" ) |
324 |
;; |
325 |
|
326 |
(defun company-fuzzy--get-backend-string (backend) |
327 |
"Get BACKEND's as a string." |
328 |
(if backend |
329 |
(let ((name (symbol-name backend))) |
330 |
(setq name (s-replace "company-" "" name) |
331 |
name (s-replace "-company" "" name)) |
332 |
name) |
333 |
"")) |
334 |
|
335 |
(defun company-fuzzy--backend-string (candidate backend) |
336 |
"Form the BACKEND string by CANDIDATE." |
337 |
(if (and company-fuzzy-show-annotation candidate) |
338 |
(let ((backend-str (company-fuzzy--get-backend-string backend))) |
339 |
(when (string-empty-p backend-str) (setq backend-str "unknown")) |
340 |
(propertize |
341 |
(format company-fuzzy-annotation-format backend-str) |
342 |
'face 'company-fuzzy-annotation-face)) |
343 |
"")) |
344 |
|
345 |
(defun company-fuzzy--source-anno-string (candidate backend) |
346 |
"Return the source annotation string by CANDIDATE and BACKEND." |
347 |
(if (and candidate backend) |
348 |
(company-fuzzy--call-backend backend 'annotation candidate) |
349 |
"")) |
350 |
|
351 |
(defun company-fuzzy--extract-annotation (candidate) |
352 |
"Extract annotation from CANDIDATE." |
353 |
(let* ((backend (company-fuzzy--get-backend-by-candidate candidate)) |
354 |
(backend-str (company-fuzzy--backend-string candidate backend)) |
355 |
(orig-anno (company-fuzzy--source-anno-string candidate backend))) |
356 |
(concat orig-anno backend-str))) |
357 |
|
358 |
;; |
359 |
;; (@* "Highlighting" ) |
360 |
;; |
361 |
|
362 |
(defun company-fuzzy--pre-render (str &optional annotation-p) |
363 |
"Prerender color with STR and flag ANNOTATION-P." |
364 |
(unless annotation-p |
365 |
(let* ((str-len (length str)) |
366 |
(prefix (company-fuzzy--backend-prefix-candidate str 'match)) |
367 |
(prefix (company-fuzzy--validate-prefix prefix)) |
368 |
(selection (or company-selection 0)) |
369 |
(cur-selection (nth selection company-candidates)) |
370 |
(splitted-section (remove "" (split-string str " "))) |
371 |
(process-selection (nth 0 splitted-section)) |
372 |
(selected (string= cur-selection process-selection)) |
373 |
(selected-face (if selected |
374 |
'company-tooltip-common-selection |
375 |
'company-tooltip-common)) |
376 |
(selected-common-face (if selected |
377 |
'company-tooltip-selection |
378 |
'company-tooltip)) |
379 |
(splitted-c (remove "" (split-string prefix "")))) |
380 |
(set-text-properties 0 str-len nil str) |
381 |
(font-lock-prepend-text-property 0 str-len 'face selected-common-face str) |
382 |
(dolist (c splitted-c) |
383 |
(let ((pos (company-fuzzy--string-match-p (regexp-quote c) str))) |
384 |
(while (and (numberp pos) (< pos str-len)) |
385 |
(font-lock-prepend-text-property pos (1+ pos) 'face selected-face str) |
386 |
(setq pos (company-fuzzy--string-match-p (regexp-quote c) str (1+ pos)))))))) |
387 |
str) |
388 |
|
389 |
;; |
390 |
;; (@* "Sorting / Scoring" ) |
391 |
;; |
392 |
|
393 |
(defun company-fuzzy--sort-prefix-on-top (candidates) |
394 |
"Sort CANDIDATES that match prefix on top of all other selection." |
395 |
(let (prefix-matches prefix) |
396 |
(dolist (cand candidates) |
397 |
(setq prefix (company-fuzzy--backend-prefix-candidate cand 'match) |
398 |
prefix (company-fuzzy--validate-prefix prefix)) |
399 |
(when (company-fuzzy--string-prefix-p prefix cand) |
400 |
(push cand prefix-matches) |
401 |
(setq candidates (remove cand candidates)))) |
402 |
(setq prefix-matches (sort prefix-matches #'string-lessp) |
403 |
candidates (append prefix-matches candidates))) |
404 |
candidates) |
405 |
|
406 |
(defun company-fuzzy--sort-candidates-by-function (candidates fnc &optional flip) |
407 |
"Sort CANDIDATES with function call FNC. |
408 |
|
409 |
If optional argument FLIP is non-nil, reverse query and pattern order." |
410 |
(let ((scoring-table (make-hash-table :test 'equal)) scoring-keys) |
411 |
(dolist (cand candidates) |
412 |
(when-let* ((prefix (company-fuzzy--backend-prefix-candidate cand 'match)) |
413 |
(scoring (or (equal prefix 'anything) |
414 |
(ignore-errors |
415 |
(if flip (funcall fnc prefix cand) |
416 |
(funcall fnc cand prefix))))) |
417 |
(score (cond ((listp scoring) (nth 0 scoring)) |
418 |
((vectorp scoring) (aref scoring 0)) |
419 |
((numberp scoring) scoring) |
420 |
(t 0)))) |
421 |
(ht-set scoring-table score (push cand (ht-get scoring-table score))))) |
422 |
;; Get all keys, and turn into a list. |
423 |
(ht-map (lambda (score-key _cands) (push score-key scoring-keys)) scoring-table) |
424 |
(setq scoring-keys (sort scoring-keys #'>) ; Sort keys in order. |
425 |
candidates nil) ; Clean up, and ready for final output. |
426 |
(dolist (key scoring-keys) |
427 |
(let ((cands (ht-get scoring-table key))) |
428 |
(setq cands (reverse cands)) ; Respect to backend order. |
429 |
(when (functionp company-fuzzy-sorting-score-function) |
430 |
(setq cands (funcall company-fuzzy-sorting-score-function cands))) |
431 |
(setq candidates (append candidates cands))))) |
432 |
candidates) |
433 |
|
434 |
(defun company-fuzzy--sort-candidates (candidates) |
435 |
"Sort all CANDIDATES base on type of sorting backend." |
436 |
;; IMPORTANT: Since the command `candidates' will change by `company-mode', |
437 |
;; we manually set the candidates here so we get can consistent result. |
438 |
(setq candidates (company-fuzzy--ht-all-candidates)) |
439 |
(when company-fuzzy-reset-selection |
440 |
(setq company-selection company-selection-default)) |
441 |
;; Don't score when it start fresh, e.g. completing a function name in Java |
442 |
;; with the . (dot) symbol |
443 |
(unless company-fuzzy--is-trigger-prefix-p |
444 |
(setq candidates |
445 |
(cl-case company-fuzzy-sorting-backend |
446 |
(`none candidates) |
447 |
(`alphabetic (sort candidates #'string-lessp)) |
448 |
(`flex |
449 |
(company-fuzzy--sort-candidates-by-function candidates #'flex-score)) |
450 |
(`flx |
451 |
(company-fuzzy--sort-candidates-by-function candidates #'flx-score)) |
452 |
(`flx-rs |
453 |
(company-fuzzy--sort-candidates-by-function candidates #'flx-rs-score)) |
454 |
(`flxy |
455 |
(company-fuzzy--sort-candidates-by-function candidates #'flxy-score)) |
456 |
((or fuz-skim fuz-clangd) |
457 |
(company-fuzzy--sort-candidates-by-function |
458 |
candidates (if (eq company-fuzzy-sorting-backend 'fuz-skim) |
459 |
#'fuz-calc-score-skim |
460 |
#'fuz-calc-score-clangd) |
461 |
t)) |
462 |
((or fuz-bin-skim fuz-bin-clangd) |
463 |
(company-fuzzy--sort-candidates-by-function |
464 |
candidates (if (eq company-fuzzy-sorting-backend 'fuz-bin-skim) |
465 |
'fuz-bin-score-skim |
466 |
'fuz-bin-score-clangd) |
467 |
t)) |
468 |
(`liquidmetal |
469 |
(company-fuzzy--sort-candidates-by-function candidates #'liquidmetal-score)) |
470 |
(`sublime-fuzzy |
471 |
(company-fuzzy--sort-candidates-by-function candidates #'sublime-fuzzy-score t)))) |
472 |
(when company-fuzzy-prefix-on-top |
473 |
(setq candidates (company-fuzzy--sort-prefix-on-top candidates))) |
474 |
(when (functionp company-fuzzy-sorting-function) |
475 |
(setq candidates (funcall company-fuzzy-sorting-function candidates)))) |
476 |
candidates) |
477 |
|
478 |
;; |
479 |
;; (@* "Completion" ) |
480 |
;; |
481 |
|
482 |
;; ZEG WEET GE WAAROM IK DIE tfuuiyf HEB TOEGEVOEGD?? |
483 |
;; OMDAT DEZE KAKFUNCTIE DAN INEENS GEFIKST IS WAUW |
484 |
(defun company-fuzzy--insert-candidate (candidate tfuuiyf) |
485 |
"Insertion for CANDIDATE." |
486 |
(when company-fuzzy-mode |
487 |
;; NOTE: Here we force to change `company-prefix' so the completion |
488 |
;; will do what we expected. |
489 |
(let ((backend (company-fuzzy--get-backend-by-candidate candidate))) |
490 |
(setq company-prefix (company-fuzzy--backend-prefix backend 'complete))))) |
491 |
|
492 |
;; |
493 |
;; (@* "Prefix" ) |
494 |
;; |
495 |
|
496 |
(defun company-fuzzy--valid-prefix (backend) |
497 |
"Guess the current BACKEND prefix." |
498 |
(let ((prefix (ht-get company-fuzzy--prefixes backend))) |
499 |
(if (stringp prefix) prefix |
500 |
(thing-at-point 'symbol)))) ; Fallback |
501 |
|
502 |
(defun company-fuzzy--validate-prefix (prefix) |
503 |
"Validate the PREFIX to proper string." |
504 |
(if (stringp prefix) ; this will handle 'anything symbol type |
505 |
prefix "")) |
506 |
|
507 |
(defun company-fuzzy--backend-prefix-complete (backend) |
508 |
"Return prefix for each BACKEND while doing completion. |
509 |
|
510 |
This function is use when function `company-fuzzy--insert-candidate' is |
511 |
called. It returns the current selection prefix to prevent completion |
512 |
completes in an odd way." |
513 |
(cl-case backend |
514 |
(`company-paths (company-fuzzy--valid-prefix backend)) |
515 |
(t (company-fuzzy--backend-prefix backend 'filter)))) |
516 |
|
517 |
(defun company-fuzzy--backend-prefix-filter (backend) |
518 |
"Return prefix for each BACKEND while doing the first basic filerting. |
519 |
|
520 |
This is some what the opposite to function `company-fuzzy--backend-prefix-get' |
521 |
since it's try get as much candidates as possible, but this function returns |
522 |
a prefix that can filter out some obvious impossible candidates." |
523 |
(cl-case backend |
524 |
(`company-capf (let* ((prefix (company-fuzzy--backend-prefix backend 'match)) |
525 |
(prefix (company-fuzzy--validate-prefix prefix))) |
526 |
prefix)) |
527 |
(`company-files (company-fuzzy--valid-prefix backend)) |
528 |
(`company-paths (company-fuzzy--backend-prefix 'company-files 'match)) |
529 |
(t (company-fuzzy--backend-prefix backend 'match)))) |
530 |
|
531 |
(defun company-fuzzy--backend-prefix-match (backend) |
532 |
"Return prefix for each BACKEND while matching candidates. |
533 |
|
534 |
This function is use for scoring and matching algorithm. It returns a prefix |
535 |
that best describe the current possible candidate. |
536 |
|
537 |
For instance, if there is a candidate function `buffer-file-name' and with |
538 |
current prefix `bfn'. It will just return `bfn' because the current prefix |
539 |
does best describe the for this candidate." |
540 |
(cl-case backend |
541 |
((company-capf) (or (company-fuzzy--valid-prefix backend) |
542 |
'anything)) |
543 |
(`company-c-headers |
544 |
(when-let ((prefix (ht-get company-fuzzy--prefixes backend))) |
545 |
;; Skip the first < or " symbol |
546 |
(substring prefix 1 (length prefix)))) |
547 |
(`company-files |
548 |
;; NOTE: For `company-files', we will return the last section of the path |
549 |
;; for the best match. |
550 |
;; |
551 |
;; Example, if I have path `/path/to/dir'; then it shall return `dir'. |
552 |
(when-let* ((prefix (ht-get company-fuzzy--prefixes backend)) |
553 |
(splitted (split-string prefix "/" t)) |
554 |
(len-splitted (length splitted)) |
555 |
(last (nth (1- len-splitted) splitted))) |
556 |
last)) |
557 |
(`company-paths |
558 |
(when-let ((prefix (ht-get company-fuzzy--prefixes backend))) |
559 |
(if (string-suffix-p "/" prefix) 'anything |
560 |
(nth 0 (last (split-string prefix "/" t)))))) |
561 |
(t company-fuzzy--prefix))) |
562 |
|
563 |
(defun company-fuzzy--backend-prefix-get (backend) |
564 |
"Return prefix for each BACKEND while getting candidates. |
565 |
|
566 |
This function is use for simplify prefix, in order to get as much candidates |
567 |
as possible for fuzzy work. |
568 |
|
569 |
For instance, if I have prefix `bfn'; then most BACKEND will not return |
570 |
function `buffer-file-name' as candidate. But with this function will use a |
571 |
letter `b' instead of full prefix `bfn'. So the BACKEND will return something |
572 |
that may be relavent to the first character `b'. |
573 |
|
574 |
P.S. Not all backend work this way." |
575 |
(cl-case backend |
576 |
(`company-c-headers |
577 |
;; Skip the < or " symbol for the first character |
578 |
(ignore-errors (substring (ht-get company-fuzzy--prefixes backend) 1 2))) |
579 |
(`company-files |
580 |
(when-let ((prefix (ht-get company-fuzzy--prefixes backend))) |
581 |
(let* ((splitted (split-string prefix "/" t)) |
582 |
(len-splitted (length splitted)) |
583 |
(last (nth (1- len-splitted) splitted)) |
584 |
(new-prefix prefix)) |
585 |
(when (< 1 len-splitted) |
586 |
(setq new-prefix |
587 |
(substring prefix 0 (- (length prefix) (length last))))) |
588 |
new-prefix))) |
589 |
(`company-paths |
590 |
(when-let ((prefix (ht-get company-fuzzy--prefixes backend))) |
591 |
(if (string-suffix-p "/" prefix) prefix |
592 |
(file-name-directory prefix)))) |
593 |
(`company-emmet (company-emmet--prefix)) |
594 |
(t |
595 |
;; Return an empty string or first character is likely going to return a |
596 |
;; full list of candaidates. And this is what we want. |
597 |
(when (ht-get company-fuzzy--prefixes backend) |
598 |
company-fuzzy--prefix-first)))) |
599 |
|
600 |
(defun company-fuzzy--backend-prefix-candidate (cand type) |
601 |
"Get the backend prefix by CAND and TYPE." |
602 |
(let ((backend (company-fuzzy--get-backend-by-candidate cand))) |
603 |
(company-fuzzy--backend-prefix backend type))) |
604 |
|
605 |
(defun company-fuzzy--backend-prefix (backend type) |
606 |
"Get the BACKEND prefix by TYPE." |
607 |
(cl-case type |
608 |
(`complete (company-fuzzy--backend-prefix-complete backend)) |
609 |
(`filter (company-fuzzy--backend-prefix-filter backend)) |
610 |
(`match (company-fuzzy--backend-prefix-match backend)) |
611 |
(`get (company-fuzzy--backend-prefix-get backend)))) |
612 |
|
613 |
;; |
614 |
;; (@* "Fuzzy Matching" ) |
615 |
;; |
616 |
|
617 |
(defun company-fuzzy--trim-trailing-re (regex) |
618 |
"Trim incomplete REGEX. |
619 |
If REGEX ends with \\|, trim it, since then it matches an empty string." |
620 |
(if (company-fuzzy--string-match "\\`\\(.*\\)[\\]|\\'" regex) (match-string 1 regex) regex)) |
621 |
|
622 |
(defun company-fuzzy--regex-fuzzy (str) |
623 |
"Build a regex sequence from STR. |
624 |
Insert .* between each char." |
625 |
(setq str (company-fuzzy--trim-trailing-re str)) |
626 |
(if (company-fuzzy--string-match "\\`\\(\\^?\\)\\(.*?\\)\\(\\$?\\)\\'" str) |
627 |
(concat (match-string 1 str) |
628 |
(let ((lst (string-to-list (match-string 2 str)))) |
629 |
(apply #'concat |
630 |
(cl-mapcar |
631 |
#'concat |
632 |
(cons "" (cdr (mapcar (lambda (c) (format "[^%c\n]*" c)) |
633 |
lst))) |
634 |
(mapcar (lambda (x) (format "\\(%s\\)" (regexp-quote (char-to-string x)))) |
635 |
lst)))) |
636 |
(match-string 3 str)) |
637 |
str)) |
638 |
|
639 |
(defun company-fuzzy--match-string (prefix candidates) |
640 |
"Return new CANDIDATES that match PREFIX." |
641 |
(when (stringp prefix) |
642 |
(let ((fuz-str (company-fuzzy--regex-fuzzy prefix)) new-cands) |
643 |
(dolist (cand candidates) |
644 |
(when (company-fuzzy--string-match-p fuz-str cand) |
645 |
(push cand new-cands))) |
646 |
new-cands))) |
647 |
|
648 |
;; |
649 |
;; (@* "Core" ) |
650 |
;; |
651 |
|
652 |
(defun company-fuzzy--ht-all-candidates () |
653 |
"Return all candidates from the data." |
654 |
(let (all-candidates) |
655 |
(ht-map (lambda (_backend cands) |
656 |
(setq all-candidates (append all-candidates cands))) |
657 |
company-fuzzy--candidates) |
658 |
(delete-dups all-candidates))) |
659 |
|
660 |
(defun company-fuzzy-all-candidates () |
661 |
"Return the list of all candidates." |
662 |
(ht-clear company-fuzzy--candidates) ; Clean up |
663 |
(setq company-fuzzy--is-trigger-prefix-p (company-fuzzy--trigger-prefix-p)) |
664 |
(dolist (backend company-fuzzy--backends) |
665 |
(if (or (company-fuzzy--lsp-passthrough backend) |
666 |
(memq backend company-fuzzy-passthrough-backends)) |
667 |
(company-fuzzy--candidates-from-passthrough-backend backend) |
668 |
(company-fuzzy--candidates-from-backend backend))) |
669 |
;; Since we insert the candidates before sorting event, see function |
670 |
;; `company-fuzzy--sort-candidates', we return to simply avoid the process |
671 |
;; from `company-mode'. |
672 |
;; |
673 |
;; This should help us save some performance! |
674 |
(when (eq this-command 'company-diag) |
675 |
;; We did return candidates here, yet this does not mean `company-diag' |
676 |
;; will respect this result. |
677 |
(company-fuzzy--ht-all-candidates))) |
678 |
|
679 |
(defun company-fuzzy--candidates-from-passthrough-backend (backend) |
680 |
"Use candidates of already fuzzy BACKEND as is." |
681 |
(let ((prefix-get (company-fuzzy--backend-prefix backend 'get)) |
682 |
temp-candidates) |
683 |
(when prefix-get |
684 |
(setq temp-candidates (company-fuzzy--call-backend backend 'candidates prefix-get))) |
685 |
(company-fuzzy--collect-candidates backend temp-candidates))) |
686 |
|
687 |
(defun company-fuzzy--candidates-from-backend (backend) |
688 |
"Do fuzzy matching for current BACKEND." |
689 |
(let ((prefix-get (company-fuzzy--backend-prefix backend 'get)) |
690 |
(prefix-fil (company-fuzzy--backend-prefix backend 'filter)) |
691 |
temp-candidates) |
692 |
(when prefix-get |
693 |
(setq temp-candidates (company-fuzzy--call-backend backend 'candidates prefix-get))) |
694 |
;; NOTE: Do the very basic filtering for speed up. |
695 |
;; |
696 |
;; The function `company-fuzzy--match-string' does the very first basic |
697 |
;; filtering in order to lower the performance before sending to function |
698 |
;; scoring engine. |
699 |
(when (and (not company-fuzzy--is-trigger-prefix-p) |
700 |
(company-fuzzy--valid-candidates-p temp-candidates) |
701 |
prefix-fil) |
702 |
(setq temp-candidates (company-fuzzy--match-string prefix-fil temp-candidates))) |
703 |
;; NOTE: Made the final completion. |
704 |
(company-fuzzy--collect-candidates backend temp-candidates))) |
705 |
|
706 |
(defun company-fuzzy--register-candidates (backend candidates) |
707 |
"Register CANDIDATES with BACKEND id." |
708 |
(delete-dups candidates) |
709 |
(ht-set company-fuzzy--candidates backend (copy-sequence candidates))) |
710 |
|
711 |
(defun company-fuzzy--collect-candidates (backend candidates) |
712 |
"Collect BACKEND's CANDIDATES by it's type." |
713 |
(cond |
714 |
;; NOTE: Asynchronous |
715 |
((company-fuzzy--async-candidates-p candidates) |
716 |
(ignore-errors |
717 |
(funcall (cdr candidates) |
718 |
(lambda (async-candidates) |
719 |
(company-fuzzy--register-candidates backend async-candidates))))) |
720 |
;; NOTE: Synchronous |
721 |
;; |
722 |
;; This is the final ensure step before processing it to scoring phase. |
723 |
;; We confirm candidates by adding it to `company-fuzzy--candidates'. |
724 |
;; The function `company-fuzzy--valid-candidates-p' is use to ensure the |
725 |
;; candidates returns a list of strings, which this is the current only valid |
726 |
;; type to this package. |
727 |
((company-fuzzy--valid-candidates-p candidates) |
728 |
(company-fuzzy--register-candidates backend candidates)))) |
729 |
|
730 |
(defun company-fuzzy--get-prefix () |
731 |
"Set the prefix just right before completion." |
732 |
(setq company-fuzzy--is-trigger-prefix-p nil |
733 |
company-fuzzy--prefix (or (ignore-errors (company-fuzzy--furthest-prefix)) |
734 |
(ignore-errors (company-fuzzy--generic-prefix)) |
735 |
(ffap-guesser)) |
736 |
company-fuzzy--prefix-first (ignore-errors (substring company-fuzzy--prefix 0 1))) |
737 |
company-fuzzy--prefix) ; make sure return it |
738 |
|
739 |
(defun company-fuzzy-all-other-backends (command &optional arg &rest ignored) |
740 |
"Backend source for all other backend except this backend, COMMAND, ARG, IGNORED." |
741 |
(interactive (list 'interactive)) |
742 |
(cl-case command |
743 |
(`interactive (company-begin-backend 'company-fuzzy-all-other-backends)) |
744 |
(`prefix (company-fuzzy--get-prefix)) |
745 |
(`annotation (company-fuzzy--extract-annotation arg)) |
746 |
(`candidates (company-fuzzy-all-candidates)) |
747 |
(`pre-render (company-fuzzy--pre-render arg (nth 0 ignored))) |
748 |
(t (company-fuzzy--backend-command arg command)))) |
749 |
|
750 |
;; |
751 |
;; (@* "Users" ) |
752 |
;; |
753 |
|
754 |
(defun company-fuzzy--ensure-local () |
755 |
"Ensure modified variable effect locally." |
756 |
(make-local-variable 'company-fuzzy--backends) |
757 |
(make-local-variable 'company-fuzzy--recorded-backends) |
758 |
(make-local-variable 'company-backends)) |
759 |
|
760 |
(defun company-fuzzy--backend-organize () |
761 |
"Organize backend after modified the backend list." |
762 |
(if company-fuzzy-mode |
763 |
(setq company-fuzzy--backends (delete-dups company-fuzzy--backends) |
764 |
company-fuzzy--recorded-backends (delete-dups company-fuzzy--recorded-backends)) |
765 |
(setq company-backends (delete-dups company-backends)))) |
766 |
|
767 |
;;;###autoload |
768 |
(defun company-fuzzy-backend-add (backend) |
769 |
"Safe way to add BACKEND." |
770 |
(company-fuzzy--ensure-local) |
771 |
(if company-fuzzy-mode |
772 |
(progn |
773 |
(add-to-list 'company-fuzzy--backends backend t) |
774 |
(add-to-list 'company-fuzzy--recorded-backends backend t)) |
775 |
(add-to-list 'company-backends backend t)) |
776 |
(company-fuzzy--backend-organize)) |
777 |
|
778 |
;;;###autoload |
779 |
(defun company-fuzzy-backend-remove (backend) |
780 |
"Safe way to remove BACKEND." |
781 |
(company-fuzzy--ensure-local) |
782 |
(if company-fuzzy-mode |
783 |
(progn |
784 |
(setq company-fuzzy--backends (cl-remove backend company-fuzzy--backends) |
785 |
company-fuzzy--recorded-backends (cl-remove backend company-fuzzy--recorded-backends))) |
786 |
(setq company-backends (cl-remove backend company-backends))) |
787 |
(company-fuzzy--backend-organize)) |
788 |
|
789 |
(defun company-fuzzy--insert-to (list elm n) |
790 |
"Insert into list LIST an element ELM at index N. |
791 |
|
792 |
If N is 0, ELM is inserted before the first element. |
793 |
|
794 |
The resulting list is returned. As the list contents is mutated |
795 |
in-place, the old list reference does not remain valid." |
796 |
(let* ((padded-list (cons nil (copy-sequence list))) |
797 |
(c (nthcdr n padded-list))) |
798 |
(setcdr c (cons elm (cdr c))) |
799 |
(cdr padded-list))) |
800 |
|
801 |
(defun company-fuzzy--insert-before (list elm new-elm) |
802 |
"Add a NEW-ELM to the LIST before ELM." |
803 |
(let ((position (or (cl-position elm list :test 'equal) 0))) |
804 |
(company-fuzzy--insert-to list new-elm position))) |
805 |
|
806 |
(defun company-fuzzy--insert-after (list elm new-elm) |
807 |
"Add a NEW-ELM to the LIST after ELM." |
808 |
(let ((position (or (cl-position elm list :test 'equal) 0))) |
809 |
(company-fuzzy--insert-to list new-elm (1+ position)))) |
810 |
|
811 |
;;;###autoload |
812 |
(defun company-fuzzy-backend-add-before (backend target) |
813 |
"Add the BACKEND before the TARGET backend." |
814 |
(company-fuzzy--ensure-local) |
815 |
(if company-fuzzy-mode |
816 |
(setq company-fuzzy--backends |
817 |
(company-fuzzy--insert-before company-fuzzy--backends |
818 |
target backend) |
819 |
company-fuzzy--recorded-backends |
820 |
(company-fuzzy--insert-before company-fuzzy--recorded-backends |
821 |
target backend)) |
822 |
(setq company-backends |
823 |
(company-fuzzy--insert-before company-backends |
824 |
target backend))) |
825 |
(company-fuzzy--backend-organize)) |
826 |
|
827 |
;;;###autoload |
828 |
(defun company-fuzzy-backend-add-after (backend target) |
829 |
"Add the BACKEND after the TARGET backend." |
830 |
(company-fuzzy--ensure-local) |
831 |
(if company-fuzzy-mode |
832 |
(setq company-fuzzy--backends |
833 |
(company-fuzzy--insert-after company-fuzzy--backends |
834 |
target backend) |
835 |
company-fuzzy--recorded-backends |
836 |
(company-fuzzy--insert-after company-fuzzy--recorded-backends |
837 |
target backend)) |
838 |
(setq company-backends |
839 |
(company-fuzzy--insert-after company-backends |
840 |
target backend))) |
841 |
(company-fuzzy--backend-organize)) |
842 |
|
843 |
;; |
844 |
;; (@* "Plugins" ) |
845 |
;; |
846 |
|
847 |
(defun company-fuzzy--lsp-connected-p () |
848 |
"Return non-nil if lsp is connected." |
849 |
(or (bound-and-true-p lsp-managed-mode) |
850 |
(bound-and-true-p eglot--managed-mode))) |
851 |
|
852 |
(defun company-fuzzy--lsp-after-enabled (&rest _) |
853 |
"Hook run after LSP is enabled." |
854 |
(when (company-fuzzy--lsp-connected-p) |
855 |
;; No need to check for `company-fuzzy-mode' is on or not since this |
856 |
;; is hook only added when `company-fuzzy-mode' is on. |
857 |
(setq-local company-backends '(company-fuzzy-all-other-backends)))) |
858 |
|
859 |
(defun company-fuzzy--lsp-passthrough (backend) |
860 |
"Respect `capf' BACKEND when LSP is available." |
861 |
(when (memq backend '(company-capf)) |
862 |
(company-fuzzy--lsp-connected-p))) |
863 |
|
864 |
(defun company-fuzzy-yasnippet--completions-for-prefix (fnc &rest args) |
865 |
"Wrap around `company-yasnippet--completions-for-prefix' function in order to |
866 |
get all possible candidates. |
867 |
|
868 |
Arguments FNC and ARGS are used to apply original operations." |
869 |
(when company-fuzzy-mode |
870 |
;; `prefix' came from `company-fuzzy--backend-prefix-get', so we simply |
871 |
;; replace set `key-prefix' to `prefix'. |
872 |
(setf (nth 1 args) (nth 0 args))) |
873 |
(apply fnc args)) |
874 |
|
875 |
(provide 'company-fuzzy) |
876 |
;;; company-fuzzy.el ends here |
877 |