rc

company-fuzzy.el

1
;;; company-fuzzy.el --- Fuzzy matching for `company-mode'  -*- lexical-binding: t; -*-
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