├── .github
└── workflows
│ └── build.yaml
├── .gitignore
├── Eask
├── LICENSE
├── Makefile
├── README.org
├── fussy-test.el
├── fussy.el
└── screenshots
└── fussy.png
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | push:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | test:
12 | runs-on: ${{ matrix.os }}
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | os: [ubuntu-latest, macos-latest, windows-latest]
17 | emacs-version:
18 | - 28.2
19 | - snapshot
20 | steps:
21 | - uses: actions/checkout@v3
22 |
23 | - uses: jcs090218/setup-emacs@master
24 | with:
25 | version: ${{ matrix.emacs-version }}
26 |
27 | - uses: emacs-eask/setup-eask@master
28 | with:
29 | version: 'snapshot'
30 |
31 | - name: Print emacs version
32 | run: |
33 | emacs --version
34 |
35 | - run: echo "$HOME/.cask/bin" >> $GITHUB_PATH
36 | - run: make install
37 | - run: make compile
38 | - run: make lint
39 | - run: make test
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.eask/
2 | /dist/
3 |
--------------------------------------------------------------------------------
/Eask:
--------------------------------------------------------------------------------
1 | ;; -*- mode: eask; lexical-binding: t -*-
2 |
3 | (package "fussy"
4 | "1.0"
5 | "Fuzzy completion style using `flx'")
6 |
7 | (website-url "https://github.com/jojojames/fussy")
8 | (keywords "matching")
9 |
10 | (package-file "fussy.el")
11 |
12 | (script "test" "echo \"Error: no test specified\" && exit 1")
13 |
14 | (source "gnu")
15 | (source "melpa")
16 |
17 | (depends-on "emacs" "28.2")
18 | (depends-on "flx")
19 | (depends-on "compat")
20 |
21 | (development
22 | (depends-on "f")
23 | (depends-on "ert-runner")
24 | (depends-on "package-lint")
25 | (depends-on "orderless"))
26 |
27 | (setq network-security-level 'low) ; see https://github.com/jcs090218/setup-emacs-windows/issues/156#issuecomment-932956432
28 |
29 | (add-hook 'eask-before-lint/package-hook
30 | (lambda (&rest _)
31 | (advice-add 'package-lint--check-eval-after-load :around 'ignore)
32 | (advice-add 'package-lint--check-version-regexp-list :around 'ignore)))
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | export EMACS ?= $(shell which emacs)
2 | CASK_DIR := $(shell cask package-directory)
3 |
4 | $(CASK_DIR): Cask
5 | cask install
6 | @touch $(CASK_DIR)
7 |
8 | .PHONY: cask
9 | cask: $(CASK_DIR)
10 |
11 | .PHONY: install
12 | install:
13 | eask package
14 | eask install
15 |
16 | .PHONY: compile
17 | compile:
18 | eask compile
19 |
20 | .PHONY: clean
21 | clean:
22 | eask clean all
23 |
24 | .PHONY: test
25 | test:
26 | eask install-deps --dev
27 | eask test ert ./fussy-test.el
28 |
29 | .PHONY: lint
30 | lint:
31 | eask lint package
32 |
--------------------------------------------------------------------------------
/README.org:
--------------------------------------------------------------------------------
1 | #+TITLE: Fussy
2 | #+STARTUP: noindent
3 |
4 | [[https://github.com/jojojames/fussy/actions/workflows/build.yaml][file:https://github.com/jojojames/fussy/actions/workflows/build.yaml/badge.svg]]
5 | [[https://melpa.org/#/fussy][file:https://melpa.org/packages/fussy-badge.svg]]
6 | [[https://stable.melpa.org/#/fussy][file:https://stable.melpa.org/packages/fussy-badge.svg]]
7 |
8 | [[./screenshots/fussy.png]]
9 |
10 | This is a package to provide a ~completion-style~ to Emacs that is able to
11 | leverage [[https://github.com/lewang/flx][flx]] as well as various other
12 | libraries such as [[https://github.com/dangduc/fzf-native][fzf-native]]
13 | to provide intelligent scoring and sorting.
14 |
15 | This package is intended to be used with packages that leverage
16 | ~completion-styles~, e.g. ~completing-read~ and ~completion-at-point-functions~.
17 |
18 | It is usable with ~icomplete~ (as well as ~fido-mode~), ~selectrum~,
19 | ~vertico~, ~corfu~, ~helm~ and ~company-mode~.
20 |
21 | It is not currently usable with ~ido~ which doesn't support
22 | ~completion-styles~ and has its own sorting and filtering system.
23 | ~ivy~ support can be somewhat baked in following
24 | https://github.com/jojojames/fussy#ivy-integration but the
25 | performance gains may not be as high as the other ~completion-read~ APIs.
26 | * Installation
27 | : M-x package-install RET fussy RET
28 | Or clone / download this repository and modify your ~load-path~:
29 |
30 | #+begin_src emacs-lisp :tangle yes
31 | (add-to-list 'load-path (expand-file-name "/path/to/fussy/" user-emacs-directory))
32 | #+end_src
33 | ** Emacs -Q example
34 | #+begin_src emacs-lisp :tangle yes
35 | (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
36 | (require 'package)
37 | (package-initialize)
38 | (package-refresh-contents)
39 |
40 | (unless (package-installed-p 'fussy)
41 | (package-install 'fussy))
42 | (fussy-setup)
43 | #+end_src
44 | ** Use-Package Example
45 | #+begin_src emacs-lisp :tangle yes
46 | (use-package fussy
47 | :ensure t
48 | :config
49 | (fussy-setup))
50 | #+end_src
51 |
52 | * Scoring Backends
53 | ~fussy~ defaults to [[https://github.com/lewang/flx][flx]] for scoring matches
54 | but it has the most integration with [[https://github.com/dangduc/fzf-native][fzf-native]].
55 |
56 | Additional (listed below) scoring libraries can also be used.
57 | ** Flx
58 | [[https://github.com/lewang/flx][flx]] is a dependency of ~fussy~ and the default
59 | scoring algorithm.
60 |
61 | ~flx~ has a great scoring algorithm but is one of the slower implementations
62 | compared to the other scoring backends written as native modules.
63 | ** Flx-rs
64 | [[https://github.com/jcs-elpa/flx-rs][flx-rs]] is a native module written in Rust
65 | that matches the original ~flx~ scoring algorithm. It is about 10 times faster
66 | than the original implementation written in Emacs Lisp. We can use this package
67 | instead for extra performance with the same scoring strategy.
68 |
69 | One downside of this package is that it doesn't yet support using ~flx~'s file
70 | cache so filename matching is currently slightly worse than the original Emacs
71 | lisp implementation.
72 |
73 | #+begin_src emacs-lisp :tangle yes
74 | (use-package flx-rs
75 | :ensure t
76 | :straight
77 | (flx-rs
78 | :repo "jcs-elpa/flx-rs"
79 | :fetcher github
80 | :files (:defaults "bin"))
81 | :config
82 | (setq fussy-score-fn 'fussy-flx-rs-score)
83 | (flx-rs-load-dyn))
84 | #+end_src
85 | ** Fzf-Native
86 | Use [[https://github.com/dangduc/fzf-native][fzf-native]] for scoring.
87 |
88 | Provides fuzzy matching scoring based on the ~fzf~ algorithm (by
89 | [[https://github.com/junegunn][junegunn]]) through a dynamic module
90 | for a native C implementation of ~fzf~,
91 | [[https://github.com/nvim-telescope/telescope-fzf-native.nvim][telescope-fzf-native.nvim]].
92 |
93 | #+begin_src emacs-lisp :tangle yes
94 | (use-package fzf-native
95 | :ensure t
96 | :straight
97 | (fzf-native
98 | :repo "dangduc/fzf-native"
99 | :host github
100 | :files (:defaults "bin"))
101 | :config
102 | (setq fussy-score-fn 'fussy-fzf-native-score)
103 | (fzf-native-load-dyn))
104 | #+end_src
105 |
106 | ** Fuz
107 | Another option is to use the [[https://github.com/rustify-emacs/fuz.el][fuz]]
108 | library (also in Rust) for scoring.
109 |
110 | This library has two fuzzy matching algorithms, ~skim~ and ~clangd~.
111 |
112 | Skim: Just like [[https://github.com/junegunn/fzf][fzf]] v2, the algorithm is
113 | based on Smith-Waterman algorithm which is normally used in DNA sequence alignment
114 |
115 | Clangd: The algorithm is based on clangd's
116 | [[https://github.com/MaskRay/ccls/blob/master/src/fuzzy_match.cc][FuzzyMatch.cpp]].
117 |
118 | For more information: [[https://github.com/lotabout/fuzzy-matcher][fuzzy-matcher]]
119 |
120 | #+begin_src emacs-lisp :tangle yes
121 | (use-package fuz
122 | :ensure nil
123 | :straight (fuz :type git :host github :repo "rustify-emacs/fuz.el")
124 | :config
125 | (setq fussy-score-fn 'fussy-fuz-score)
126 | (unless (require 'fuz-core nil t)
127 | (fuz-build-and-load-dymod)))
128 | #+end_src
129 |
130 | #+begin_src emacs-lisp :tangle yes
131 | ;; Same as fuz but with prebuilt binaries.
132 | (use-package fuz-bin
133 | :ensure t
134 | :straight
135 | (fuz-bin
136 | :repo "jcs-elpa/fuz-bin"
137 | :fetcher github
138 | :files (:defaults "bin"))
139 | :config
140 | (setq fussy-score-fn 'fussy-fuz-bin-score)
141 | (fuz-bin-load-dyn))
142 | #+end_src
143 | ** Liquid Metal
144 | This is the algorithm used by the old [[https://www.emacswiki.org/emacs/lusty-explorer.el][lusty-explorer]].
145 |
146 | A mimetic poly-alloy of the Quicksilver scoring algorithm,
147 | essentially LiquidMetal.
148 |
149 | Flex matching short abbreviations against longer strings is a boon in
150 | productivity for typists. Applications like Quicksilver, Alfred, LaunchBar, and
151 | Launchy have made this method of keyboard entry a popular one. It's time to
152 | bring this same functionality to web controls. LiquidMetal makes scoring long
153 | strings against abbreviations easy.
154 |
155 | For more information: [[https://github.com/rmm5t/liquidmetal][liquidmetal]]
156 |
157 | #+begin_src emacs-lisp :tangle yes
158 | (use-package liquidmetal
159 | :ensure t
160 | :straight t
161 | :config
162 | (setq fussy-score-fn 'fussy-liquidmetal-score))
163 | #+end_src
164 |
165 | ** Sublime-Fuzzy
166 | Fuzzy matching algorithm based on Sublime Text's string search.
167 | Iterates through characters of a search string and calculates a score.
168 | This is another fuzzy implementation written in Rust.
169 |
170 | For more information: [[https://github.com/Schlechtwetterfront/fuzzy-rs][fuzzy-rs]]
171 |
172 | #+begin_src emacs-lisp :tangle yes
173 | (use-package sublime-fuzzy
174 | :ensure t
175 | :straight
176 | (sublime-fuzzy
177 | :repo "jcs-elpa/sublime-fuzzy"
178 | :fetcher github
179 | :files (:defaults "bin"))
180 | :config
181 | (setq fussy-score-fn 'fussy-sublime-fuzzy-score)
182 | (sublime-fuzzy-load-dyn))
183 | #+end_src
184 | ** Hotfuzz
185 | This is a fuzzy Emacs completion style similar to the built-in flex style, but
186 | with a better scoring algorithm. Specifically, it is non-greedy and ranks
187 | completions that match at word; path component; or camelCase boundaries higher.
188 |
189 | For more information: [[https://github.com/axelf4/hotfuzz][hotfuzz]]
190 |
191 | Note, ~hotfuzz~ has its own ~completion-style~ that may be worth using over this one.
192 |
193 | #+begin_src emacs-lisp :tangle yes
194 | (use-package hotfuzz
195 | :ensure t
196 | :straight t
197 | :config
198 | (setq fussy-score-fn 'fussy-hotfuzz-score))
199 | #+end_src
200 | * Company Integration
201 | Call ~fussy-company-setup~. This function advises a few ~company-mode~ functions.
202 | #+begin_src emacs-lisp :tangle yes
203 | (fussy-company-setup)
204 | #+end_src
205 | * Corfu Integration
206 | #+begin_src emacs-lisp :tangle yes
207 | ;; For cache functionality.
208 | (advice-add 'corfu--capf-wrapper :before 'fussy-wipe-cache)
209 |
210 | (add-hook 'corfu-mode-hook
211 | (lambda ()
212 | (setq-local fussy-max-candidate-limit 5000
213 | fussy-default-regex-fn 'fussy-pattern-first-letter
214 | fussy-prefer-prefix nil)))
215 | #+end_src
216 | * Eglot Integration
217 | #+begin_src emacs-lisp :tangle yes
218 | (fussy-eglot-setup)
219 | #+end_src
220 | * Helm Integration
221 | Integration with [[https://github.com/emacs-helm/helm][helm]] is possible by
222 | setting ~helm-completion-style~ to ~emacs~ instead of ~helm~.
223 |
224 | #+begin_src emacs-lisp :tangle yes
225 | (setq helm-completion-style 'emacs)
226 | #+end_src
227 |
228 | For more information:
229 | https://github.com/emacs-helm/helm/blob/master/helm-mode.el#L269
230 |
231 | * Icomplete/Fido Integration
232 | ~fido~ uses the built in ~flex~ ~completion-style~ by default. We can advise
233 | ~icomplete~'s setup hook to set up ~fussy~ with ~fido-mode~.
234 |
235 | #+begin_src emacs-lisp :tangle yes
236 | (use-package icomplete
237 | :ensure nil
238 | :straight nil
239 | :config
240 | (defun fussy-fido-setup ()
241 | "Use `fussy' with `fido-mode'."
242 | (setq-local completion-styles '(fussy basic)))
243 | (advice-add 'icomplete--fido-mode-setup :after 'fussy-fido-setup)
244 | (setq icomplete-tidy-shadowed-file-names t
245 | icomplete-show-matches-on-no-input t
246 | icomplete-compute-delay 0
247 | icomplete-delay-completions-threshold 50)
248 | ;; Or `fido-mode'.
249 | (fido-vertical-mode))
250 | #+end_src
251 | * Ivy Integration
252 | Since ~ivy~ doesn't support ~completion-styles~, we have to hack ~fussy~ into it.
253 | We can advise ~ivy--flx-sort~ and replace it with our own sorting function.
254 |
255 | #+begin_src emacs-lisp :tangle yes
256 | (defun ivy--fussy-sort (name cands)
257 | "Sort according to closeness to string NAME the string list CANDS."
258 | (condition-case nil
259 | (let* ((bolp (= (string-to-char name) ?^))
260 | ;; An optimized regex for fuzzy matching
261 | ;; "abc" → "^[^a]*a[^b]*b[^c]*c"
262 | (fuzzy-regex (concat "\\`"
263 | (and bolp (regexp-quote (substring name 1 2)))
264 | (mapconcat
265 | (lambda (x)
266 | (setq x (char-to-string x))
267 | (concat "[^" x "]*" (regexp-quote x)))
268 | (if bolp (substring name 2) name)
269 | "")))
270 | ;; Strip off the leading "^" for flx matching
271 | (flx-name (if bolp (substring name 1) name))
272 | cands-left
273 | cands-to-sort)
274 |
275 | ;; Filter out non-matching candidates
276 | (dolist (cand cands)
277 | (when (string-match-p fuzzy-regex cand)
278 | (push cand cands-left)))
279 |
280 | ;; pre-sort the candidates by length before partitioning
281 | (setq cands-left (cl-sort cands-left #'< :key #'length))
282 |
283 | ;; partition the candidates into sorted and unsorted groups
284 | (dotimes (_ (min (length cands-left) ivy-flx-limit))
285 | (push (pop cands-left) cands-to-sort))
286 |
287 | (nconc
288 | ;; Compute all of the flx scores in one pass and sort
289 | (mapcar #'car
290 | (sort (mapcar
291 | (lambda (cand)
292 | (cons cand
293 | (car
294 | (funcall
295 | fussy-score-fn
296 | cand flx-name
297 | ivy--flx-cache))))
298 | cands-to-sort)
299 | (lambda (c1 c2)
300 | ;; Break ties by length
301 | (if (/= (cdr c1) (cdr c2))
302 | (> (cdr c1)
303 | (cdr c2))
304 | (< (length (car c1))
305 | (length (car c2)))))))
306 | ;; Add the unsorted candidates
307 | cands-left))
308 | (error cands)))
309 |
310 | (advice-add 'ivy--flx-sort :override 'ivy--fussy-sort)
311 | #+end_src
312 |
313 | For more information: https://github.com/abo-abo/swiper/issues/848#issuecomment-1143129670
314 |
315 | * Recommendations
316 | ~fussy~ is written to be configure-less by the user. For defaults, it uses the
317 | built-in ~flex~ algorithm for filtering and ~flx~ for scoring and sorting.
318 |
319 | However, users are encouraged to try the various available scoring backends.
320 | These scoring backends are configured through ~fussy-score-fn~. See its docstring
321 | for configuration.
322 |
323 | For improved performance, use a scoring backend backed by a native module.
324 | Examples include but are not limited to:
325 |
326 | - ~flx-rs~
327 | - ~fuz/fuz-bin~
328 | - ~fzf-native~
329 |
330 | ~flx-rs~ will provide an algorithm that matches the original ~flx~ algorithm
331 | while the other two matches other popular packages (~skim~ and ~fzf~).
332 |
333 | Below is a sample config that uses ~flx-rs~ for improved performance.
334 |
335 | ~fuz-bin~ or ~fuz~ may be a better choice for performance than ~flx-rs~ but uses
336 | a different algorithm.
337 |
338 | #+begin_src emacs-lisp :tangle yes
339 | (use-package orderless
340 | :straight t
341 | :ensure t
342 | :commands (orderless-filter))
343 |
344 | (use-package flx-rs
345 | :ensure t
346 | :straight
347 | (flx-rs
348 | :repo "jcs-elpa/flx-rs"
349 | :fetcher github
350 | :files (:defaults "bin"))
351 | :config
352 | (setq fussy-score-fn 'fussy-flx-rs-score)
353 | (flx-rs-load-dyn))
354 |
355 | (use-package fussy
356 | :ensure t
357 | :straight
358 | (fussy :type git :host github :repo "jojojames/fussy")
359 | :config
360 | (setq fussy-score-fn 'fussy-flx-rs-score)
361 | (setq fussy-filter-fn 'fussy-filter-orderless-flex)
362 | (fussy-setup)
363 | (fussy-eglot-setup)
364 | (fussy-company-setup))
365 | #+end_src
366 |
367 | For the most performant option, use [[https://github.com/dangduc/fzf-native][fzf-native]]
368 | and see my configuration for an example. See Benchmarking below for basic runs.
369 | * My Configuration
370 | Documenting my configuration for the users that may want to copy. Unlike the
371 | former configuration, this section will be kept up to date with my ~init.el~.
372 |
373 | #+begin_src emacs-lisp :tangle yes
374 | (use-package fzf-native
375 | :ensure
376 | (:repo "dangduc/fzf-native"
377 | :host github
378 | :files (:defaults "bin"))
379 | :config
380 | (fzf-native-load-dyn)
381 | (setq fussy-score-fn 'fussy-fzf-native-score))
382 |
383 | (use-package company
384 | :config
385 | (global-company-mode))
386 |
387 | (use-package fussy
388 | :ensure
389 | (fussy :host github :repo "jojojames/fussy")
390 | :config
391 | (setq fussy-score-ALL-fn 'fussy-fzf-score)
392 | (setq fussy-filter-fn 'fussy-filter-default)
393 | (setq fussy-use-cache t)
394 | (setq fussy-compare-same-score-fn 'fussy-histlen->strlen<)
395 | (fussy-setup)
396 | (fussy-eglot-setup)
397 | (fussy-company-setup))
398 | #+end_src
399 | * Scoring Samples
400 | Listed below are samples of scores that backends return given a candidate string and a search string to match against it.
401 | This may help in determining a preferred scoring backend.
402 |
403 | Please PR other examples as they come up. This score can be obtained by commenting out the log message in ~fussy-score~.
404 | Another way to do it is to feed candidates and queries into ~fussy-score~ with the desired ~fussy-score-fn~.
405 | ** Fuz
406 | #+begin_src emacs-lisp :tangle yes
407 | ;; candidate: Makefile query: mkfile score 77
408 | ;; candidate: fork/yasnippet-snippets/snippets/chef-mode/cookbook_file query: mkfile score 68
409 | #+end_src
410 | ** Fzf
411 | #+begin_src emacs-lisp :tangle yes
412 | ;; candidate: Makefile query: mkfile 118
413 | ;; candidate: fork/yasnippet-snippets/snippets/chef-mode/cookbook_file query: mkfile 128
414 | #+end_src
415 |
416 | * Filtering Choices
417 | Before scoring and sorting candidates, we must somehow filter them from the
418 | completion table. The approaches below are several ways to do that, each with
419 | varying advantages and disadvantages.
420 |
421 | For the choices below, we benchmark the functions by benchmarking the entire
422 | ~fussy-all-completions~ function with the below macro calling ~M-x
423 | describe-symbol (30000 candidates)~ in the scratch buffer.
424 |
425 | #+begin_src emacs-lisp :tangle yes
426 | (defmacro fussy--measure-time (&rest body)
427 | "Measure the time it takes to evaluate BODY.
428 | https://lists.gnu.org/archive/html/help-gnu-emacs/2008-06/msg00087.html"
429 | `(let ((time (current-time)))
430 | (let ((result ,@body))
431 | (message "%.06f" (float-time (time-since time)))
432 | result)))
433 | #+end_src
434 |
435 | ** Flex
436 | This is the default filtering method and is 1:1 to the filtering done
437 | when using the ~flex~ ~completion-style~. Advantages are no additional
438 | dependencies (e.g. ~orderless~) and likely bug-free/stable to use.
439 |
440 | The only disadvantage is that it's the slowest of the filtering methods.
441 |
442 | #+begin_src emacs-lisp :tangle yes
443 |
444 | ;; Flex
445 | (setq fussy-filter-fn 'fussy-filter-flex)
446 | ;; Type Letter a
447 | ;; 0.078952
448 | ;; Type Letter b
449 | ;; 0.052590
450 | ;; Type Letter c
451 | ;; 0.065808
452 | ;; Type Letter d
453 | ;; 0.061254
454 | ;; Type Letter e
455 | ;; 0.098000
456 | ;; Type Letter f
457 | ;; 0.053321
458 | ;; Type Letter g
459 | ;; 0.050180
460 | #+end_src
461 |
462 | ** Fast
463 | This is another usable filtering method and leverages the ~all-completions~ API
464 | written in C to do its filtering. It seems to be the fastest of the filtering
465 | methods from quick benchmarking as well as requiring no additional dependencies
466 | (e.g. ~orderless~).
467 |
468 | Implementation may be buggy though, so use with caution.
469 |
470 | #+begin_src emacs-lisp :tangle yes
471 | ;; Fast
472 | (setq fussy-filter-fn 'fussy-filter-default)
473 | ;; Type Letter a
474 | ;; 0.030671
475 | ;; Type Letter b
476 | ;; 0.030247
477 | ;; Type Letter c
478 | ;; 0.036047
479 | ;; Type Letter d
480 | ;; 0.032071
481 | ;; Type Letter e
482 | ;; 0.034785
483 | ;; Type Letter f
484 | ;; 0.030392
485 | ;; Type Letter g
486 | ;; 0.033473
487 | #+end_src
488 | ** Orderless
489 | [[https://github.com/oantolin/orderless][orderless]] can also be used for
490 | filtering. It uses the ~all-completions~ API like ~fussy-filter-default~ so is
491 | also faster than the default filtering but has a dependency on ~orderless~.
492 |
493 | #+begin_src emacs-lisp :tangle yes
494 | ;; Orderless
495 | (setq fussy-filter-fn 'fussy-filter-orderless-flex)
496 | ;; Type Letter a
497 | ;; 0.065390
498 | ;; Type Letter b
499 | ;; 0.036942
500 | ;; Type Letter c
501 | ;; 0.054091
502 | ;; Type Letter d
503 | ;; 0.048816
504 | ;; Type Letter e
505 | ;; 0.074258
506 | ;; Type Letter f
507 | ;; 0.040900
508 | ;; Type Letter g
509 | ;; 0.037928
510 | #+end_src
511 |
512 | To use [[https://github.com/oantolin/orderless][orderless]] filtering:
513 |
514 | #+begin_src emacs-lisp :tangle yes
515 | (use-package orderless
516 | :straight t
517 | :ensure t
518 | :commands (orderless-filter))
519 |
520 | (setq fussy-filter-fn 'fussy-filter-orderless)
521 |
522 | ;; Highlight matches with `company-mode'.
523 | (with-eval-after-load 'orderless
524 | ;; https://www.reddit.com/r/emacs/comments/nichkl/how_to_use_different_completion_styles_in_the/
525 | ;; https://github.com/oantolin/orderless#company
526 | (defun orderless-just-one-face (fn &rest args)
527 | (let ((orderless-match-faces [completions-common-part]))
528 | (ignore orderless-match-faces)
529 | (apply fn args)))
530 | (advice-add 'company-capf--candidates
531 | :around #'orderless-just-one-face))
532 | #+end_src
533 | * Caching
534 | Results and filtering can be cached for improved performance by setting
535 | ~fussy-use-cache~ to t.
536 |
537 | With this set to t:
538 |
539 | If user already entered the same query:
540 |
541 | e.g. User types "a" -> "ab" and then backspaces into "a" again.
542 |
543 | Results from the originally entered "a" will be used for the second entered "a".
544 |
545 | If user is entering a new query but there exists results from a previous query
546 | in the cache:
547 |
548 | e.g. User types "a" and then "ab". Results from "a" will then be used for
549 | filtering in "ab".
550 |
551 | To use this with ~company~ and ~corfu~, use an advice to reset the cache upon
552 | new completion requests.
553 |
554 | #+begin_src emacs-lisp :tangle yes
555 | (advice-add 'corfu--capf-wrapper :before 'fussy-wipe-cache)
556 | (fussy-company-setup)
557 | #+end_src
558 | * Benchmarking
559 | #+begin_src emacs-lisp :tangle yes
560 | (setq random-col (all-completions "" 'help--symbol-completion-table nil))
561 |
562 | (benchmark-run 10 (dolist (x random-col)
563 | (flx-score x "a")))
564 | (29.064313 37 3.8456069999999993)
565 |
566 | (benchmark-run 10 (dolist (x random-col)
567 | (fussy-fzf-native-score x "a")))
568 | (5.763323 2 0.2168050000000008)
569 |
570 | ;; Handles entire list at once.
571 | (benchmark-run 10 (fussy-fzf-score random-col "a"))
572 | (0.33876900000000004 0 0.0)
573 | #+end_src
574 | * Contributing
575 | Set up ~eask~.
576 | #+begin_src sh :tangle yes
577 | $ brew install node
578 | $ npm install -g @emacs-eask/eask
579 | #+end_src
580 | #+begin_src emacs-lisp :tangle yes
581 | make test
582 | #+end_src
583 | * Discussions
584 | https://github.com/lewang/flx/issues/54
585 | https://github.com/company-mode/company-mode/issues/47
586 | https://github.com/abo-abo/swiper/issues/207
587 | https://github.com/abo-abo/swiper/issues/2321
588 | https://github.com/abo-abo/swiper/issues/848
589 | https://github.com/melpa/melpa/pull/8029
590 | https://github.com/emacs-helm/helm/issues/2165
591 |
--------------------------------------------------------------------------------
/fussy-test.el:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jojojames/fussy/4b4d26661e460cc6351a81b8186d3f7c29c64453/fussy-test.el
--------------------------------------------------------------------------------
/fussy.el:
--------------------------------------------------------------------------------
1 | ;;; fussy.el --- Fuzzy completion style using `flx' -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright 2022 James Nguyen
4 |
5 | ;; Author: James Nguyen
6 | ;; Version: 1.0
7 | ;; Package-Requires: ((emacs "28.2") (flx "0.5") (compat "30.0.0.0"))
8 | ;; Keywords: matching
9 | ;; Homepage: https://github.com/jojojames/fussy
10 |
11 | ;; This program is free software; you can redistribute it and/or modify
12 | ;; it under the terms of the GNU General Public License as published by
13 | ;; the Free Software Foundation, either version 3 of the License, or
14 | ;; (at your option) any later version.
15 |
16 | ;; This program is distributed in the hope that it will be useful,
17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | ;; GNU General Public License for more details.
20 |
21 | ;; You should have received a copy of the GNU General Public License
22 | ;; along with this program. If not, see .
23 |
24 | ;;; Commentary:
25 |
26 | ;; This is a fuzzy Emacs completion style similar to the built-in
27 | ;; `flex' style, but using `flx' for scoring. It also supports various other
28 | ;; fuzzy scoring systems in place of `flx'.
29 |
30 | ;; This package is intended to be used with packages that leverage
31 | ;; `completion-styles', e.g. `completing-read' and
32 | ;; `completion-at-point-functions'.
33 |
34 | ;; It is usable with `icomplete' (as well as `fido-mode'), `selectrum',
35 | ;; `vertico', `corfu', `helm' and `company-mode''s `company-capf'.
36 |
37 | ;; It is not currently usable with `ido' which doesn't support
38 | ;; `completion-styles' and has its own sorting and filtering system. In
39 | ;; addition to those packages, other `company-mode' backends will not hook into
40 | ;; this package. `ivy' support can be somewhat baked in following
41 | ;; https://github.com/jojojames/fussy#ivy-integration but the
42 | ;; performance gains may not be as high as the other `completion-read' APIs.
43 |
44 | ;; To use this style, prepend `fussy' to `completion-styles'.
45 |
46 | ;; For improved performance,`fussy-filter-fn' and `fussy-score-fn' for filtering
47 | ;; and scoring matches are good initial starting points for customization.
48 |
49 | ;; The various available scoring backends in `fussy-score-fn' have varying
50 | ;; levels of performance and match quality.
51 | ;; For a faster version that implements the same matching as `flx', use
52 | ;; https://github.com/jcs-elpa/flx-rs which is a native module written in Rust.
53 |
54 | ;; Other notable scoring backends supported by this package:
55 | ;; flx: https://github.com/lewang/flx
56 | ;; fzf: https://github.com/junegunn/fzf
57 | ;; skim: https://github.com/lotabout/fuzzy-matcher
58 |
59 | ;; For an exhaustive list of scoring backends, take a look at
60 | ;; https://github.com/jojojames/fussy#scoring-backends
61 |
62 | (require 'flx)
63 | (require 'compat)
64 | (eval-when-compile (require 'subr-x))
65 |
66 | ;;; Code:
67 |
68 | (declare-function "orderless-filter" "orderless")
69 | (declare-function "orderless-highlight-matches" "orderless")
70 | (declare-function "orderless--prefix+pattern" "orderless")
71 | (defvar orderless-matching-styles)
72 |
73 | ;;
74 | ;; (@* "Landmarks" )
75 | ;;
76 |
77 | ;; `fussy-all-completions'
78 | ;; `fussy-score'
79 | ;; `fussy-filter-default'
80 |
81 | ;;
82 | ;; (@* "Customizations" )
83 | ;;
84 |
85 | (defgroup fussy nil
86 | "Fuzzy completion style using `flx.'."
87 | :group 'flx
88 | :link '(url-link :tag "GitHub" "https://github.com/jojojames/fussy"))
89 |
90 | (defcustom fussy-max-query-length 100
91 | "Collections with queries longer than this are not scored using `flx'.
92 |
93 | See `fussy-all-completions' for implementation details."
94 | :group 'fussy
95 | :type 'integer)
96 |
97 | (defcustom fussy-max-candidate-limit 30000
98 | "Apply optimizations for collections greater than this limit.
99 |
100 | `fussy-all-completions' will apply some optimizations.
101 |
102 | N -> this variable's value
103 |
104 | 1. The collection (to be scored) will initially be filtered based on
105 | `fussy-max-limit-preferred-candidate-fn'.
106 |
107 | 2. Score only up to N * `fussy-percent-of-candidates-to-score' words.
108 | The rest won't be scored.
109 |
110 | Additional implementation details:
111 | https://github.com/abo-abo/swiper/issues/207#issuecomment-141541960"
112 | :group 'fussy
113 | :type 'integer)
114 |
115 | (defcustom fussy-percent-of-candidates-to-score .7
116 | "When `fussy-max-candidate-limit' is hit, this variable determines the %
117 | of candidates out of all candidates to score. For example, if
118 | `fussy-max-candidate-limit' is 30000 and the collection is 40000, the # of
119 | candidates to score will be 28000."
120 | :group 'fussy
121 | :type 'number)
122 |
123 | (defcustom fussy-ignore-case t
124 | "If t, ignores `completion-ignore-case'.
125 |
126 | If this is set to nil, highlighting may break for cases where we're
127 | highlighting with `completion-pcm--hilit-commonality'."
128 | :group 'fussy
129 | :type 'boolean)
130 |
131 | (defcustom fussy-score-threshold-to-filter nil
132 | "Candidates with scores of N or less are filtered.
133 |
134 | Some backends such as `fussy-fuz-score' return negative scores
135 | for low-quality matches.
136 |
137 | If this is set to nil, threshold is defined by alist of
138 | thresholds for score functions. Set this to a number to override
139 | `fussy-score-threshold-to-filter-alist'.
140 |
141 | Raise N to see fewer candidates. Lower N to see more
142 | candidates. Keep N at 0 or more for performance."
143 | :group 'fussy
144 | :type 'integer)
145 |
146 | (defcustom fussy-score-threshold-to-filter-alist
147 | '((flx-score . -100)
148 | (fussy-fuz-score . -100)
149 | (fussy-fuz-bin-score . -100)
150 | (fussy-fzf-native-score . 0)
151 | (fussy-hotfuzz-score . 0))
152 | "Candidates with scores of N or less are filtered for a given
153 | `fussy-score-fn'.
154 |
155 | Some backends such as `fussy-fuz-score' return negative scores
156 | for low-quality matches.
157 |
158 | Setting `fussy-score-threshold-to-filter' to a number will
159 | override this alist.
160 |
161 | If `fussy-score-fn' is not in the mapping, default to a threshold
162 | of 0 wherever alist is used."
163 | :group 'fussy
164 | :type 'alist)
165 |
166 | (defcustom fussy-max-word-length-to-score 400
167 | "Words that are longer than this length are not scored."
168 | :group 'fussy
169 | :type 'integer)
170 |
171 | (defcustom fussy-propertize-fn
172 | #'fussy-propertize-common-part
173 | "Function used to propertize matches.
174 |
175 | Takes STR \(to be propertized\) and
176 | SCORE \(list of indices of STR to be propertized\).
177 |
178 | This function is expected to return STR.
179 |
180 | If this is nil, don't propertize (e.g. highlight matches) at all.
181 | This can also be set to nil to assume highlighting from a different source.
182 |
183 | e.g. `fussy-filter-orderless' can also be used for highlighting matches."
184 | :type `(choice
185 | (const :tag "No highlighting" nil)
186 | (const :tag "By completions-common face."
187 | ,#'fussy-propertize-common-part)
188 | (const :tag "By flx propertization." ,'flx-propertize)
189 | (function :tag "Custom function"))
190 | :group 'fussy)
191 |
192 | (defcustom fussy-compare-same-score-fn
193 | #'fussy-histlen->strlen<
194 | "Function used to compare matches with the same \\='completion-score.
195 |
196 | FN takes in and compares two candidate strings C1 and C2 and
197 | returns which candidates should have precedence.
198 |
199 | If this is nil, do nothing."
200 | :type `(choice
201 | (const :tag "Don't compare candidates with same score." nil)
202 | (const :tag "Shorter candidates have precedence."
203 | ,#'fussy-strlen<)
204 | (const :tag "Longer candidates have precedence."
205 | ,#'fussy-strlen>)
206 | (const :tag "Recent candidates have precedence."
207 | ,#'fussy-histlen<)
208 | (const :tag "Recent (then shorter length) candidates have precedence."
209 | ,#'fussy-histlen->strlen<)
210 | (function :tag "Custom function"))
211 | :group 'fussy)
212 |
213 | (defcustom fussy-max-limit-preferred-candidate-fn nil
214 | "Function used when collection length is greater than\
215 |
216 | `fussy-max-candidate-limit'.
217 |
218 | FN takes in and compares two candidate strings C1 and C2 and
219 | returns which candidates should have precedence.
220 |
221 | If this is nil, take the first `fussy-max-candidate-limit' number
222 | of candidates that was returned by the completion table."
223 | :type `(choice
224 | (const :tag "Take the first X number of candidates." nil)
225 | (const :tag "Shorter candidates have precedence."
226 | ,#'fussy-strlen<)
227 | (const :tag "Longer candidates have precedence."
228 | ,#'fussy-strlen>)
229 | (const :tag "Recent candidates have precedence."
230 | ,#'fussy-histlen<)
231 | (const :tag "Recent (then shorter length) candidates have precedence."
232 | ,#'fussy-histlen->strlen<)
233 | (function :tag "Custom function"))
234 | :group 'fussy)
235 |
236 | (defcustom fussy-filter-fn
237 | #'fussy-filter-flex
238 | "Function used for filtering candidates before scoring.
239 |
240 | FN takes in the same arguments as `fussy-try-completions'.
241 |
242 | This FN should not be nil.
243 |
244 | Use either `fussy-filter-orderless' or `fussy-filter-default' for faster
245 | filtering through the `all-completions' (written in C) interface.
246 |
247 | If using `fussy-filter-default', `fussy-default-regex-fn' can be configured."
248 | :type `(choice
249 | (const :tag "Built in Flex Filtering"
250 | ,#'fussy-filter-flex)
251 | (const :tag "Built in Faster Flex Filtering in C"
252 | ,#'fussy-filter-default)
253 | (const :tag "Orderless Flex Filtering"
254 | ,#'fussy-filter-orderless-flex)
255 | (const :tag "Orderless"
256 | ,#'fussy-filter-orderless)
257 | (function :tag "Custom function"))
258 | :group 'fussy)
259 |
260 | (defcustom fussy-default-regex-fn
261 | #'fussy-pattern-default
262 | "Function used to create regex for `fussy-filter-default'.
263 |
264 | It takes in a STR and returns a regex usable with `all-completions'.
265 |
266 | The return value of this FN is meant to be pushed to `completion-regexp-list'.
267 |
268 | Flex 1 is what is used in `company-flx'. It seems to be the fastest from an eye
269 | test but all the regex are comparable in performance.
270 |
271 | Flex 2 functions match the regex returned by `orderless-flex'. Flex 2 functions
272 | are more exhaustive than Flex 1 functions."
273 | :type `(choice
274 | (const :tag "Flex 1"
275 | ,#'fussy-pattern-flex-1)
276 | (const :tag "Flex 2"
277 | ,#'fussy-pattern-flex-2)
278 | (const :tag "Default"
279 | ,#'fussy-pattern-default)
280 | (const :tag "First Letter"
281 | ,#'fussy-pattern-first-letter)
282 | (function :tag "Custom function"))
283 | :group 'fussy)
284 |
285 | (defcustom fussy-score-fn
286 | 'flx-score
287 | "Function used for scoring candidates.
288 |
289 | FN should at least take in STR and QUERY.
290 |
291 | This may or may not be used by `fussy-score-ALL-fn'."
292 | :type `(choice
293 | (const :tag "Score using Flx"
294 | ,'flx-score)
295 | (const :tag "Score using Flx-RS"
296 | ,#'fussy-flx-rs-score)
297 | (const :tag "Score using FZF"
298 | ,'fussy-fzf-native-score)
299 | (const :tag "Score using Fuz"
300 | ,#'fussy-fuz-score)
301 | (const :tag "Score using Fuz-Bin"
302 | ,#'fussy-fuz-bin-score)
303 | (const :tag "Score using LiquidMetal"
304 | ,#'fussy-liquidmetal-score)
305 | (const :tag "Score using Sublime-Fuzzy"
306 | ,#'fussy-sublime-fuzzy-score)
307 | (const :tag "Score using Hotfuzz"
308 | ,#'fussy-hotfuzz-score)
309 | (function :tag "Custom function"))
310 | :group 'fussy)
311 |
312 | (defcustom fussy-whitespace-ok-fns '(fussy-fzf-native-score)
313 | "List of `fussy-score-fn's that can accept whitespace."
314 | :type '(list function)
315 | :group 'fussy)
316 |
317 | (defcustom fussy-score-ALL-fn 'fussy-score
318 | "Function used for score ALL candidates.
319 |
320 | FN should take in ARGS: candidates string &optional cache.
321 |
322 | This function may call out to `fussy-score-fn' to score matches or
323 | does the heavy lifting itself.
324 |
325 | For example `fussy-score' makes use of `fussy-score-fn' but
326 | `fussy-fzf-score' sends its entire collection to `fzf-native' instead."
327 | :type `(choice
328 | (const :tag "Default scoring"
329 | ,'fussy-score)
330 | (const :tag "Scoring using `fzf-native-score-all'."
331 | ,#'fussy-fzf-score)
332 | (function :tag "Custom function"))
333 | :group 'fussy)
334 |
335 | (defcustom fussy-fuz-use-skim-p t
336 | "If t, use skim fuzzy matching algorithm with `fuz'.
337 |
338 | If nil, use clangd fuzzy matching algorithm with `fuz'.
339 |
340 | This boolean is only used if `fussy-fuz-score' is the `fussy-score-fn'."
341 | :group 'fussy
342 | :type 'boolean)
343 |
344 | (defcustom fussy-score-fns-without-indices '(fussy-hotfuzz-score
345 | fussy-sublime-fuzzy-score
346 | fussy-liquidmetal-score)
347 | "List of scoring functions that only returns the score.
348 |
349 | e.g. Instead of returning LIST SCORE MATCH_1 MATCH_2 which something like
350 | `flx-score' does, it returns LIST SCORE.
351 |
352 | Scoring functions in this list's highlighting are then taken care of by either
353 |
354 | `fussy-filter-orderless' or `completion-pcm--hilit-commonality'. See
355 |
356 | `fussy--use-pcm-highlight-p'.
357 |
358 | Functions in this list should match `fussy-score-fn'."
359 | :type '(list function)
360 | :group 'fussy)
361 |
362 | (defcustom fussy-remove-bad-char-fn
363 | #'fussy-without-tofu-char
364 | "Function used to strip characters that some backends are unable to handle.
365 |
366 | Some scoring backends \(e.g. Rust backends\) are unable to handle strings with
367 | certain character encoding. This function is applied to the candidate strings
368 | before they are passed to the scoring function.
369 |
370 | This was added specifically for `consult' but other encodings could also pose
371 | a problem. To keep the performance of the Rust backends useful,
372 | `fussy-without-tofu-char' is set as the default function.
373 | `fussy-without-tofu-char' is an order of magnitude faster than
374 | `fussy-without-unencodeable-chars' but won't handle every case.
375 |
376 | Another option is to use `fussy-encode-coding-string' which dumbly converts
377 | a multibytestring without considering what the final string will look like.
378 | Using this may work for the purpose of matching too as the final candidate
379 | string may go from something like abcX to abcR where X was the multibyte char
380 | that is not usable with the above scoring backends and R is a random ascii
381 | character encoded from X.
382 |
383 | This is set to nil if `fussy-setup' is called as we use the workaround
384 | described here:
385 | https://github.com/axelf4/hotfuzz?tab=readme-ov-file#dynamic-module
386 | You can set this again if another encoding proves to be a problem.
387 |
388 | For more information: \(https://github.com/minad/consult/issues/585\)"
389 | :type `(choice
390 | (const :tag "Remove Tofu"
391 | ,#'fussy-without-tofu-char)
392 | (const :tag "Remove All"
393 | ,#'fussy-without-unencodeable-chars)
394 | (const :tag "Convert to Unibyte"
395 | ,#'fussy-encode-coding-string)
396 | (const :tag "Don't convert"
397 | nil)
398 | (function :tag "Custom function"))
399 | :group 'fussy)
400 |
401 | (defcustom fussy-prefer-prefix t
402 | "When using `fussy-filter-default', whether to prefer infix or prefix.
403 |
404 | If t, prefix is used with `all-completions', if nil, use infix.
405 |
406 | Infix is generally faster for `all-completions' but is not exhaustive.
407 | Prefix can be slower but is exhaustive. For `completing-read',exhaustive
408 | filtering is generally more preferable but for `completion-at-point-functions',
409 | using infix can be a good tradeoff.
410 |
411 | This variable should be let-bound/wrapped over `completion-at-point-functions',
412 | e.g. `company-capf' and set to nil for typing performance and kept to t for
413 | normal `completing-read' scenarios.
414 |
415 | See comments in `fussy-filter-default' for examples of what infix or prefix
416 | can look like."
417 | :type 'boolean
418 | :group 'fussy)
419 |
420 | (defcustom fussy-filter-unscored-candidates t
421 | "Whether or not to filter unscored candidates.
422 |
423 | This only applies when `fussy-max-candidate-limit' is reached."
424 | :type 'boolean
425 | :group 'fussy)
426 |
427 | (defcustom fussy-use-cache nil
428 | "Whether or not to use cache in `fussy-all-completions'."
429 | :type 'boolean
430 | :group 'fussy)
431 |
432 | (defcustom fussy-company-prefix-length 4
433 | "The prefix length before using `fussy' with `company'."
434 | :group 'fussy
435 | :type 'integer)
436 |
437 | ;;;###autoload
438 | (defcustom fussy-adjust-metadata-fn
439 | #'fussy--adjust-metadata
440 | "Used for `completion--adjust-metadata' to adjust completion metadata.
441 |
442 | `completion--adjust-metadata' is what is used to set up sorting of candidates
443 | based on `completion-score'. The default `flex' completion style in
444 | `completion-styles' uses `completion--flex-adjust-metadata' which respects
445 | the original completion table's sort functions:
446 |
447 | e.g. display-sort-function, cycle-sort-function
448 |
449 | The default of `fussy-adjust-metadata-fn' is used instead to ignore
450 | existing sort functions in favor of sorting based only on the scoring done by
451 | `fussy-score-fn'."
452 | :type `(choice
453 | (const :tag "Adjust metadata using fussy."
454 | ,#'fussy--adjust-metadata)
455 | (const :tag "Adjust metadata using flex."
456 | ,#'completion--flex-adjust-metadata)
457 | (function :tag "Custom function"))
458 | :group 'fussy)
459 |
460 | (defmacro fussy--measure-time (&rest body)
461 | "Measure the time it takes to evaluate BODY.
462 | https://lists.gnu.org/archive/html/help-gnu-emacs/2008-06/msg00087.html"
463 | `(let ((time (current-time)))
464 | (let ((result ,@body))
465 | (message "%.06f" (float-time (time-since time)))
466 | result)))
467 |
468 | ;;
469 | ;; (@* "defsubst" )
470 | ;;
471 |
472 |
473 | (defsubst fussy-encode-coding-string (string)
474 | "Call `encode-coding-string' for STRING."
475 | (encode-coding-string string 'utf-8 t))
476 |
477 | (defsubst fussy-without-bad-char (str)
478 | "Return STR without bad characters in them."
479 | (or (and fussy-remove-bad-char-fn
480 | (funcall fussy-remove-bad-char-fn str))
481 | str))
482 |
483 | ;;
484 | ;; (@* "Constants and Variables" )
485 | ;;
486 |
487 | (defvar completion-lazy-hilit)
488 | (defvar completion-lazy-hilit-fn)
489 |
490 | (defvar-local fussy--hist-hash nil
491 | "Hash table representing `minibuffer-history-variable'.
492 |
493 | KEYs are values in the list.
494 | VALUES are positions of the values in the list.
495 |
496 | See `fussy--history-hash-table'.")
497 |
498 | (defvar-local fussy--score-threshold-to-filter-alist-cache nil
499 | "Cached value of threshold derived from alist for score functions.
500 |
501 | If `fussy-score-threshold-to-filter' is non-nil, the cache is
502 | ignored.
503 |
504 | See `fussy-score-threshold-to-filter-alist'.")
505 |
506 | (defvar-local fussy--all-cache nil
507 | "Hash table representing a cache for `fussy-all-completions'.")
508 |
509 | (defvar-local fussy-can-adjust-metadata-p t
510 | "Variable to flip whether or not `fussy' can adjust metadata.
511 |
512 | This is intended to be let-bound by users when they don't want any sorting.
513 |
514 | See `fussy--adjust-metadata' for more details.")
515 |
516 | ;;
517 | ;; (@* "All Completions Interface/API" )
518 | ;;
519 |
520 | ;;;###autoload
521 | (defun fussy-try-completions (string table pred point)
522 | "Try to flex-complete STRING in TABLE given PRED and POINT.
523 |
524 | Implement `try-completions' interface by using `completion-flex-try-completion'."
525 | ;; (message "called `fussy-try-completions'...")
526 | (completion-flex-try-completion string table pred point))
527 |
528 | ;;;###autoload
529 | (defun fussy-all-completions (string table pred point)
530 | "Get flex-completions of STRING in TABLE, given PRED and POINT.
531 |
532 | Implement `all-completions' interface with additional fuzzy / `flx' scoring."
533 | ;; (message "called `fussy-all-completions'...")
534 | (setf fussy--hist-hash (fussy--history-hash-table))
535 | (when (and fussy-use-cache
536 | (or
537 | (not fussy--all-cache)
538 | (equal string "")))
539 | (setf fussy--all-cache
540 | (make-hash-table :test 'equal)))
541 | (when fussy-ignore-case
542 | ;; `completion-ignore-case' is usually set up in `minibuffer-with-setup-hook'.
543 | ;; e.g. `read-file-name-default'
544 | ;; Many search functions leverage this variable. In the case of fuzzy
545 | ;; matching, it is better to match insensitively.
546 | ;; For example, the implementation of `completion-pcm--hilit-commonality'
547 | ;; uses `case-fold-search' which sets its value to `completion-ignore-case'.
548 | ;; Other examples include `completion-pcm--all-completions' which is used by
549 | ;; `fussy-filter-flex'. `orderless-filter' and `all-completions' also use
550 | ;; this variable.
551 | (setq-local completion-ignore-case t))
552 | (let* ((metadata (completion-metadata string table pred))
553 | (cache (if (memq (completion-metadata-get metadata 'category)
554 | '(file
555 | project-file))
556 | flx-file-cache
557 | flx-strings-cache))
558 | (beforepoint (substring string 0 point))
559 | (afterpoint (substring string point))
560 | (bounds (completion-boundaries beforepoint table pred afterpoint))
561 | (prefix (substring beforepoint 0 (car bounds)))
562 | (infix (concat
563 | (substring beforepoint (car bounds))
564 | (substring afterpoint 0 (cdr bounds)))))
565 | (if-let ((cached-all (and fussy-use-cache
566 | (cl-copy-list
567 | (gethash string fussy--all-cache)))))
568 | (progn
569 | ;; (message "%s from hash with length %d"
570 | ;; string (length cached-all))
571 | ;; (fussy--print-hash-table fussy--all-cache)
572 | (nconc (fussy--highlight-collection
573 | (if (fussy--orderless-p)
574 | (fussy--recreate-orderless-pattern
575 | string table pred point)
576 | (fussy--recreate-regex-pattern
577 | beforepoint afterpoint bounds))
578 | cached-all)
579 | (length prefix)))
580 | (pcase
581 | (while-no-input
582 | (pcase-let*
583 | ((`(,all ,pattern ,_prefix)
584 | (if-let ((cached-all
585 | (and
586 | fussy-use-cache
587 | (length> string 0)
588 | ;; e.g. ~/.emacs.d/url/ should not use entry from "~/.emacs.d/url".
589 | (not (string-suffix-p "/" string))
590 | (cl-copy-list
591 | (gethash
592 | (substring string 0 (- (length string) 1))
593 | fussy--all-cache)))))
594 | (progn
595 | ;; (message "using cache for filter")
596 | (list
597 | cached-all
598 | (if (fussy--orderless-p)
599 | (fussy--recreate-orderless-pattern
600 | string table pred point)
601 | (fussy--recreate-regex-pattern
602 | beforepoint afterpoint bounds))
603 | prefix))
604 | (funcall fussy-filter-fn
605 | string table pred point))))
606 | ;; (message (format
607 | ;; "fn: %S string: %s prefix: %s infix: %s all: %S pattern: %s"
608 | ;; 'fussy-all-completions
609 | ;; string prefix infix (or all '("nada")) pattern))
610 | (when all
611 | (if (or (length> infix fussy-max-query-length)
612 | (string= infix ""))
613 | (fussy--highlight-collection pattern all)
614 | (if (length< all fussy-max-candidate-limit)
615 | (fussy--highlight-collection
616 | pattern
617 | (fussy-outer-score all infix cache))
618 | (let ((unscored-candidates '())
619 | (candidates-to-score '()))
620 | ;; Presort candidates by
621 | ;; `fussy-max-limit-preferred-candidate-fn'.
622 | (setf unscored-candidates
623 | (if fussy-max-limit-preferred-candidate-fn
624 | (sort
625 | all fussy-max-limit-preferred-candidate-fn)
626 | ;; If `fussy-max-limit-preferred-candidate-fn'
627 | ;; is nil, we'll partition the candidates as is.
628 | all))
629 | ;; Partition the candidates into sorted and unsorted groups.
630 | (dotimes (_n (* (length unscored-candidates)
631 | fussy-percent-of-candidates-to-score))
632 | (push (pop unscored-candidates) candidates-to-score))
633 | (append
634 | ;; Compute all of the fuzzy scores only for candidates.
635 | (fussy--highlight-collection
636 | pattern
637 | (fussy-outer-score candidates-to-score infix cache))
638 | unscored-candidates)))))))
639 | ('nil
640 | ;; (message "fn: %S nil" 'fussy-all-completions)
641 | nil)
642 | ('t
643 | ;; (message "fn: %S quoteT" 'fussy-all-completions)
644 | nil)
645 | (`,collection
646 | ;; (message (format "fn: %S collection: %s"
647 | ;; 'fussy-all-completions collection))
648 | ;; Collection can be 0 when there are no candidates returned.
649 | (when (consp collection)
650 | (when fussy-use-cache
651 | ;; (message "putting %s into hash with coll length %s"
652 | ;; string (length collection))
653 | ;; (fussy--print-hash-table fussy--all-cache)
654 | (puthash string (cl-copy-list collection)
655 | fussy--all-cache))
656 | (nconc collection (length prefix))))))))
657 |
658 | ;;
659 | ;; (@* "Scoring & Highlighting" )
660 | ;;
661 |
662 | (defun fussy-valid-score-p (score)
663 | "Return whether SCORE is valid."
664 | (and score
665 | ;; Score of '(nil) can be returned...
666 | (car score)
667 | (> (car score)
668 | (or fussy-score-threshold-to-filter
669 | fussy--score-threshold-to-filter-alist-cache
670 | (setq fussy--score-threshold-to-filter-alist-cache
671 | (or (alist-get
672 | fussy-score-fn
673 | fussy-score-threshold-to-filter-alist)
674 | 0))))))
675 |
676 | (defun fussy-outer-score (candidates string &optional cache)
677 | "Function used to wrap `fussy-score-ALL-fn'."
678 | (funcall fussy-score-ALL-fn candidates string cache))
679 |
680 | (defun fussy-fzf-score (candidates string &optional _cache)
681 | "Score and propertize CANDIDATES using STRING.
682 |
683 | This implementation uses `fzf-native-score-all' to do all its scoring in one go.
684 |
685 | Ignore CACHE. This is only added to match `fussy-score'."
686 | (when (fboundp 'fzf-native-score-all)
687 | (let ((string (fussy-encode-coding-string string)))
688 | (fzf-native-score-all candidates string))))
689 |
690 | (defun fussy-score (candidates string &optional cache)
691 | "Score and propertize CANDIDATES using STRING.
692 |
693 | Use CACHE for scoring.
694 |
695 | Set a text-property \='completion-score on candidates with their score.
696 | `completion--adjust-metadata' later uses this \='completion-score for sorting."
697 | (let ((result '())
698 | (string (fussy-encode-coding-string
699 | (if (memq fussy-score-fn fussy-whitespace-ok-fns)
700 | string
701 | (replace-regexp-in-string "\\\s" "" string)))))
702 | (dolist (x candidates)
703 | (setf x (copy-sequence x))
704 | (if (> (length x) fussy-max-word-length-to-score)
705 | ;; Don't score x but don't filter it out either.
706 | (unless fussy-filter-unscored-candidates
707 | (push x result))
708 | (let ((score (funcall fussy-score-fn x string cache)))
709 | ;; (message
710 | ;; (format "fn: %S candidate: %s query: %s score %S"
711 | ;; 'fussy-score x string score))
712 | ;; Candidates with a score of N or less are filtered.
713 | (when (fussy-valid-score-p score)
714 | (put-text-property 0 1 'completion-score (car score) x)
715 |
716 | ;; If we're using pcm highlight, we don't need to propertize the
717 | ;; string here. This is faster than the pcm highlight but doesn't
718 | ;; seem to work with `find-file'.
719 | (when (fussy--should-propertize-p)
720 | (setf
721 | x (funcall fussy-propertize-fn x score)))
722 | (push x result)))))
723 | ;; Returns nil if empty.
724 | result))
725 |
726 | (defun fussy--should-propertize-p ()
727 | "Whether or not to call `fussy-propertize-fn'.
728 |
729 | If `fussy--use-pcm-highlight-p' is t, highlighting will be handled in
730 | `fussy--maybe-highlight'.
731 |
732 | If `fussy--orderless-p' is t, `fussy-filter-orderless' will take care of
733 | highlighting.
734 |
735 | If `fussy-propertize-fn' is nil, no highlighting should take place."
736 | (and
737 | (not (fussy--use-pcm-highlight-p))
738 | (not (fussy--orderless-p))
739 | fussy-propertize-fn))
740 |
741 | (defun fussy-orderless--highlight-collection (regexps completions ignore-case)
742 | "Highlight COMPLETIONS using REGEXPS respecting IGNORE-CASE.
743 |
744 | This is extracted from `orderless-all-completions' to do highlighting.
745 | `orderless' returns the filtered collection immediately which lets it do its
746 | highlighting after filtering. Since we sort and score the collection afterwards,
747 | we need to highlight the collection later.
748 |
749 | E.g. In `orderless': filter -> highlight -> return collection
750 | In `fussy', filter* -> score# -> sort# -> highlight* -> return collection.
751 |
752 | The * is taken care of by `orderless' and the # is taken care of by `fussy'.
753 |
754 | The names of the parameters REGEXPS and COMPLETIONS match `orderless' to make it
755 | easy to compare with the original but they are 1:1 with
756 | `fussy--highlight-collection''s PATTERN and COLLECTION parameters."
757 | (when (fboundp 'orderless--highlight)
758 | (if completion-lazy-hilit
759 | (setq completion-lazy-hilit-fn
760 | (apply-partially #'orderless--highlight regexps ignore-case))
761 | (cl-loop for str in-ref completions do
762 | (setf str (orderless--highlight
763 | regexps ignore-case (substring str))))))
764 | completions)
765 |
766 | (defun fussy--highlight-collection (pattern collection)
767 | "Highlight COLLECTION using PATTERN.
768 |
769 | Only highlight if `fussy--use-pcm-highlight-p' is t."
770 | (when collection
771 | (cond
772 | ((fussy--use-pcm-highlight-p)
773 | (fussy--pcm-highlight pattern collection))
774 | ((fussy--orderless-p)
775 | (fussy-orderless--highlight-collection
776 | pattern collection completion-ignore-case))
777 | (:default
778 | ;; Assume that the collection's highlighting is handled elsewhere.
779 | collection))))
780 |
781 | (defun fussy--pcm-highlight (pattern collection)
782 | "Highlight with pcm-style for COLLECTION using PATTERN.
783 |
784 | pcm-style refers to using `completion-pcm--hilit-commonality' for highlighting."
785 | (completion-pcm--hilit-commonality pattern collection))
786 |
787 | (defun fussy-propertize-common-part (str score)
788 | "Return propertized copy of STR according to score.
789 |
790 | If SCORE does not have indices to highlight, return STR unmodified."
791 | (if (or
792 | ;; Has only score but no indices or nil.
793 | (<= (length score) 1)
794 | ;; Indices are higher than the length of str indicating the indices are
795 | ;; incorrect. Skip highlighting to avoid breaking completion.
796 | ;; Take the last index to compare against str because all indices need
797 | ;; to be less than the length of str in order for highlighting to work.
798 | (>= (car (last score)) (length str)))
799 | str
800 | ;; Has a score and an index to highlight.
801 | (let ((block-started (cadr score))
802 | (last-char nil)
803 | ;; Originally we used `substring-no-properties' when setting str but
804 | ;; that strips text properties that other packages may set.
805 | ;; One example is `consult', which sprinkles text properties onto
806 | ;; the candidate. e.g. `consult--line-prefix' will check for
807 | ;; 'consult-location on str candidate.
808 | (str (if (consp str) (car str) str)))
809 | (dolist (char (cdr score))
810 | (when (and last-char
811 | (not (= (1+ last-char) char)))
812 | (add-face-text-property block-started (1+ last-char)
813 | 'completions-common-part nil str)
814 | (setf block-started char))
815 | (setf last-char char))
816 | (add-face-text-property block-started (1+ last-char)
817 | 'completions-common-part nil str)
818 | (when (and
819 | last-char
820 | (> (length str) (+ 2 last-char)))
821 | (add-face-text-property (1+ last-char) (+ 2 last-char)
822 | 'completions-first-difference
823 | nil
824 | str))
825 | (if (consp str)
826 | (cons str (cdr str))
827 | str))))
828 |
829 | ;;
830 | ;; (@* "Bootstrap" )
831 | ;;
832 |
833 | ;;;###autoload
834 | (progn
835 | (put 'fussy 'completion--adjust-metadata fussy-adjust-metadata-fn)
836 | (add-to-list 'completion-styles-alist
837 | '(fussy fussy-try-completions fussy-all-completions
838 | "Smart Fuzzy completion with scoring.")))
839 |
840 | ;;;###autoload
841 | (defun fussy-setup ()
842 | "Set up `fussy'."
843 | (unless (memq 'fussy completion-styles)
844 | (push 'fussy completion-styles))
845 |
846 | ;; https://github.com/minad/consult/issues/585
847 | ;; https://github.com/axelf4/hotfuzz?tab=readme-ov-file#dynamic-module
848 | (setq fussy-remove-bad-char-fn nil)
849 | (with-eval-after-load 'consult
850 | (defvar consult--tofu-char)
851 | (defvar consult--tofu-range)
852 | (setq consult--tofu-char #x100000
853 | consult--tofu-range #x00fffe))
854 |
855 | ;; For example, project-find-file uses 'project-files which uses
856 | ;; substring completion by default. Set our own defaults.
857 | (setq completion-category-overrides
858 | '((buffer
859 | (styles fussy basic))
860 | (unicode-name
861 | (styles fussy basic))
862 | (project-file
863 | (styles fussy))
864 | (xref-location
865 | (styles fussy))
866 | (info-menu
867 | (styles fussy basic))
868 | (symbol-help
869 | (styles fussy basic)))))
870 |
871 | ;;
872 | ;; (@* "Sorting" )
873 | ;;
874 |
875 | (defun fussy--adjust-metadata (metadata)
876 | "If actually doing filtering, adjust METADATA's sorting."
877 | (let ((flex-is-filtering-p
878 | ;; JT@2019-12-23: FIXME: this is kinda wrong. What we need
879 | ;; to test here is "some input that actually leads/led to
880 | ;; flex filtering", not "something after the minibuffer
881 | ;; prompt". E.g. The latter is always true for file
882 | ;; searches, meaning we'll be doing extra work when we
883 | ;; needn't.
884 | (and
885 | fussy-can-adjust-metadata-p
886 | (or (not (window-minibuffer-p))
887 | (> (point-max) (minibuffer-prompt-end))))))
888 | `(metadata
889 | ,@(and flex-is-filtering-p
890 | `((display-sort-function . fussy--sort)))
891 | ,@(and flex-is-filtering-p
892 | `((cycle-sort-function . fussy--sort)))
893 | ,@(cdr metadata))))
894 |
895 | (defun fussy--sort (completions)
896 | "Sort COMPLETIONS using `completion-score' and completion length."
897 | (sort
898 | completions
899 | (lambda (c1 c2)
900 | (let ((s1 (or (get-text-property 0 'completion-score c1) 0))
901 | (s2 (or (get-text-property 0 'completion-score c2) 0)))
902 | ;; (message (format "c1: %s score: %d" c1 s1))
903 | ;; (message (format "c2: %s score: %d" c2 s2))
904 | (if (and (= s1 s2)
905 | fussy-compare-same-score-fn)
906 | (funcall fussy-compare-same-score-fn c1 c2)
907 | ;; Candidates with higher completion score have precedence.
908 | (> s1 s2))))))
909 |
910 | ;;
911 | ;; (@* "Candidate Comparisons" )
912 | ;;
913 |
914 | (defun fussy-strlen< (c1 c2)
915 | "Return t if C1's length is less than C2's length."
916 | (< (length c1) (length c2)))
917 |
918 | (defun fussy-strlen> (c1 c2)
919 | "Return t if C1's length is greater than C2's length."
920 | (> (length c1) (length c2)))
921 |
922 | (defun fussy-histlen< (c1 c2)
923 | "Return t if C1 occurred more recently than C2.
924 |
925 | Check C1 and C2 in `minibuffer-history-variable' which is stored in
926 | `fussy--hist-hash'."
927 | (if-let* ((hist fussy--hist-hash)
928 | (c1-pos (or (gethash c1 hist) most-positive-fixnum))
929 | (c2-pos (or (gethash c2 hist) most-positive-fixnum)))
930 | (< c1-pos c2-pos)
931 | nil))
932 |
933 | (defun fussy-histlen->strlen< (c1 c2)
934 | "Return t if C1 occurs more recently than C2 or is shorter than C2."
935 | (if-let* ((hist fussy--hist-hash)
936 | (c1-pos (or (gethash c1 hist) most-positive-fixnum))
937 | (c2-pos (or (gethash c2 hist) most-positive-fixnum)))
938 | (if (= c1-pos c2-pos)
939 | (fussy-strlen< c1 c2)
940 | (< c1-pos c2-pos))
941 | (fussy-strlen< c1 c2)))
942 |
943 | ;;
944 | ;; (@* "Utils" )
945 | ;;
946 |
947 | (defun fussy--recreate-orderless-pattern (string table pred _point)
948 | "See `fussy--recreate-regex-pattern'."
949 | ;; This implementation from `orderless-all-completions'.
950 | (if (fboundp 'orderless--compile)
951 | (pcase-let
952 | ((`(,_prefix ,regexps ,_ignore-case ,_pred)
953 | (if (eq fussy-filter-fn 'fussy-filter-orderless-flex)
954 | (let ((orderless-matching-styles '(orderless-flex)))
955 | (ignore orderless-matching-styles)
956 | (orderless--compile string table pred))
957 | (orderless--compile string table pred))))
958 | regexps)
959 | nil))
960 |
961 | (defun fussy--recreate-regex-pattern (beforepoint afterpoint bounds)
962 | "Utility function to create regex pattern for highlighting.
963 |
964 | `fussy--highlight-collection' consumes this pattern.
965 | This usually comes out as a result of the initial filtering of candidates,
966 | but when we're pulling from the cache, the pattern is not there, so we
967 | rebuild it here. We could also try caching the pattern instead of creating it
968 | again."
969 | (cond
970 | ((eq fussy-filter-fn 'fussy-filter-flex)
971 | ;; This comes from `completion-substring--all-completions'
972 | ;; Look at `fussy-filter-flex'.
973 | (let* ((basic-pattern (completion-basic--pattern
974 | beforepoint afterpoint bounds))
975 | (pattern (if (not (stringp (car basic-pattern)))
976 | basic-pattern
977 | (cons 'prefix basic-pattern)))
978 | (pattern
979 | (completion-pcm--optimize-pattern
980 | (completion-flex--make-flex-pattern pattern))))
981 | pattern))
982 | (:default ;; `fussy-filter-default'
983 | (fussy-make-pcm-highlight-pattern
984 | beforepoint afterpoint bounds))))
985 |
986 | (defun fussy--orderless-p ()
987 | "Return whether or not we're using `orderless' for filtering."
988 | (or (eq fussy-filter-fn 'fussy-filter-orderless)
989 | (eq fussy-filter-fn 'fussy-filter-orderless-flex)))
990 |
991 | (defun fussy--use-pcm-highlight-p ()
992 | "Check if highlighting should use `completion-pcm--hilit-commonality'.
993 |
994 | Check if `fussy-score-fn' used doesn't return match indices.
995 | Check if `orderless' is being used."
996 | (cond
997 | ;; If we're using `orderless' to filter, don't use pcm highlights because
998 | ;; `orderless' does it on its own.
999 | ((fussy--orderless-p) nil)
1000 | ;; `fussy-fzf-score' doesn't highlight on its own.
1001 | ((eq fussy-score-ALL-fn 'fussy-fzf-score) t)
1002 | ;; These don't generate match indices to highlight at all so we should
1003 | ;; highlight with `completion-pcm--hilit-commonality'.
1004 | ((memq fussy-score-fn fussy-score-fns-without-indices) t)
1005 | (:default nil)))
1006 |
1007 | (defun fussy--history-hash-table ()
1008 | "Return hash table representing `minibuffer-history-variable'.
1009 |
1010 | Key is the history string and Value is the history position."
1011 | (when-let* ((hist (and (not (eq minibuffer-history-variable t))
1012 | (symbol-value minibuffer-history-variable)))
1013 | (table (make-hash-table :test 'equal
1014 | :size (length hist))))
1015 | (cl-loop for index from 0
1016 | for item in hist
1017 | unless (gethash item table)
1018 | do (puthash item index table))
1019 | table))
1020 |
1021 | (defun fussy-without-unencodeable-chars (string)
1022 | "Strip invalid chars from STRING.
1023 |
1024 | See `fussy-remove-bad-char-fn'."
1025 | ;; https://emacs.stackexchange.com/questions/5732/how-to-strip-invalid-utf-8-characters-from-a-string
1026 | (string-join
1027 | (delq nil (mapcar (lambda (ch)
1028 | (encode-coding-char ch 'utf-8 'unicode))
1029 | string))))
1030 |
1031 | (defconst fussy--consult--tofu-char #x200000
1032 | "Special character used to encode line prefixes for disambiguation.
1033 | We use invalid characters outside the Unicode range.")
1034 |
1035 | (defconst fussy--consult--tofu-range #x100000
1036 | "Special character range.")
1037 |
1038 | (defsubst fussy--consult--tofu-p (char)
1039 | "Return non-nil if CHAR is a tofu."
1040 | (<= fussy--consult--tofu-char char
1041 | (+ fussy--consult--tofu-char fussy--consult--tofu-range -1)))
1042 |
1043 | (defun fussy-without-tofu-char (string)
1044 | "Strip unencodeable char from STRING.
1045 |
1046 | See `fussy-remove-bad-char-fn'."
1047 | (if (fussy--consult--tofu-p (aref string (- (length string) 1)))
1048 | (substring string 0 (- (length string) 1))
1049 | string))
1050 |
1051 | (defun fussy--print-hash-table (table)
1052 | "Print TABLE."
1053 | (message "------------------------------------------------------------------")
1054 | (maphash (lambda (key value)
1055 | (message "key: %s # of elements: %s" key (length value)))
1056 | table)
1057 | (message "------------------------------------------------------------------"))
1058 |
1059 | (defun fussy-wipe-cache (&rest _)
1060 | "Wipe buffer local `fussy--all-cache'."
1061 | ;; (message "Setting `fussy--all-cache' to nil..")
1062 | (setf fussy--all-cache nil))
1063 |
1064 | ;;
1065 | ;; (@* "Filtering" )
1066 | ;;
1067 |
1068 | (defun fussy-filter-orderless-flex (string table pred point)
1069 | "Match STRING to the entries in TABLE.
1070 |
1071 | Use `orderless' for filtering by passing STRING, TABLE and PRED to
1072 |
1073 | `orderless-filter'. _POINT is not used. This version sets up `orderless'
1074 | to only use the `orderless-flex' pattern."
1075 | (require 'orderless)
1076 | (let ((orderless-matching-styles '(orderless-flex)))
1077 | (fussy-filter-orderless string table pred point)))
1078 |
1079 | (defun fussy-filter-orderless (string table pred _point)
1080 | "Match STRING to the entries in TABLE.
1081 |
1082 | Use `orderless' for filtering by passing STRING, TABLE and PRED to
1083 |
1084 | `orderless-filter'. _POINT is not used."
1085 | (require 'orderless)
1086 | (when (and (fboundp 'orderless--filter)
1087 | (fboundp 'orderless--compile))
1088 | (pcase-let ((`(,prefix ,regexps ,ignore-case ,pred)
1089 | (orderless--compile string table pred)))
1090 | (when-let ((completions (orderless--filter
1091 | prefix regexps ignore-case table pred)))
1092 | (list completions regexps prefix)))))
1093 |
1094 | (defun fussy-filter-flex (string table pred point)
1095 | "Match STRING to the entries in TABLE.
1096 |
1097 | Respect PRED and POINT. The filter here is the same as in
1098 | `completion-flex-all-completions'."
1099 | (pcase-let ((`(,completions ,pattern ,prefix ,_suffix ,_carbounds)
1100 | (completion-substring--all-completions
1101 | string
1102 | table pred point
1103 | #'completion-flex--make-flex-pattern)))
1104 | (list completions pattern prefix)))
1105 |
1106 | (defun fussy-filter-default (string table pred point)
1107 | "Match STRING to the entries in TABLE.
1108 |
1109 | Respect PRED and POINT. This filter uses the `all-completions' interface
1110 | that's written in C for faster filtering."
1111 | (let* ((beforepoint (substring string 0 point))
1112 | (afterpoint (substring string point))
1113 | (bounds (completion-boundaries beforepoint table pred afterpoint))
1114 | (prefix (substring beforepoint 0 (car bounds)))
1115 | (infix (concat
1116 | (substring beforepoint (car bounds))
1117 | (substring afterpoint 0 (cdr bounds))))
1118 | (regexp (funcall fussy-default-regex-fn infix))
1119 | (completion-regexp-list regexp)
1120 | ;; Commentary on why we prefer prefix over infix.
1121 | ;; For `find-file', if the prefix exists, we're in a different
1122 | ;; directory, so should be retrieving candidates from that directory
1123 | ;; instead.
1124 | ;; ex. We started in ~/ home directory. User starts typing cod.
1125 | ;; infix will be: c -> co -> cod
1126 | ;; prefix will be ~/
1127 | ;; User then enters a directory called ~/Code and types abc.
1128 | ;; infix will be: a -> ab -> abc
1129 | ;; prefix will be ~/Code
1130 | ;; For `project-find-file', the prefix will usually be empty and only
1131 | ;; the infix will be matched against.
1132 | ;; So, *knock on wood*, it seems safe to prefer prefix completion over
1133 | ;; infix completion.
1134 | (completions
1135 | ;; Is there an easier way to check if string is empty or nil?
1136 | (if (or (/= (length prefix) 0)
1137 | fussy-prefer-prefix)
1138 | ;; Always use prefix if available for correctness.
1139 | ;; For example, `find-file', should always use prefix.
1140 | (or (all-completions prefix table pred)
1141 | (all-completions infix table pred))
1142 | ;; When prefix is nil, the choice if infix or prefix is preference..
1143 | ;; Infix is much faster than prefix but can be "wrong" or not
1144 | ;; exhaustive for matches. Prefix will be exhaustive and "correct"
1145 | ;; but can be slow. Generally, we should prefer prefix for
1146 | ;; correctness.
1147 | ;; We allow an escape hatch to infix for extra performance with
1148 | ;; `fussy-prefer-prefix' set to nil.
1149 | (or (all-completions infix table pred)
1150 | (all-completions prefix table pred))))
1151 | ;; Create this pattern for the sole purpose of highlighting with
1152 | ;; `completion-pcm--hilit-commonality'. We don't actually need this
1153 | ;; for `all-completions' to work since we're just using
1154 | ;; `completion-regexp-list' with `all-completions'.
1155 | ;; In addition to that, we only need this pattern if we're highlighting
1156 | ;; using `completion-pcm--hilit-commonality' so skip evaluating the
1157 | ;; pattern if this is not the pcm highlight case.
1158 | (pattern
1159 | (fussy-make-pcm-highlight-pattern beforepoint afterpoint bounds)))
1160 | ;; (message
1161 | ;; (format
1162 | ;; "prefix: %s infix: %s pattern %s completions %S regexp_list: %S"
1163 | ;; prefix infix pattern completions completion-regexp-list))
1164 | (list completions pattern prefix)))
1165 |
1166 | (defun fussy-make-pcm-highlight-pattern (beforepoint afterpoint bounds)
1167 | "Create flex pattern for highlighting.
1168 |
1169 | Respect BEFOREPOINT, AFTERPOINT, and BOUNDS."
1170 | (when (fussy--use-pcm-highlight-p)
1171 | ;; Note to self:
1172 | ;; The way we create the pattern here can be found in
1173 | ;; `completion-substring--all-completions'.
1174 | (let* ((basic-pattern (completion-basic--pattern
1175 | beforepoint afterpoint bounds))
1176 | (pattern (if (not (stringp (car basic-pattern)))
1177 | basic-pattern
1178 | (cons 'prefix basic-pattern))))
1179 | (completion-pcm--optimize-pattern
1180 | (completion-flex--make-flex-pattern pattern)))))
1181 |
1182 | ;;
1183 | ;; (@* "Pattern Compiler" )
1184 | ;;
1185 | ;; Random note:
1186 | ;; These return something similar to what `orderless-pattern-compiler'
1187 | ;; These can be applied where `orderless-pattern-compiler' can apply.
1188 | ;; e.g. They return \(list some-regex\).
1189 | ;;
1190 |
1191 | (defun fussy-pattern-flex-1 (str)
1192 | "Make STR flex pattern.
1193 |
1194 | This may be the fastest regex to use but is not exhaustive."
1195 | (list
1196 | (concat "\\`"
1197 | (mapconcat
1198 | (lambda (x)
1199 | (setf x (string x))
1200 | (concat "[^" x "]*" (regexp-quote x)))
1201 | str
1202 | ""))))
1203 |
1204 | (defun fussy-pattern-flex-2 (str)
1205 | "Make STR flex pattern.
1206 |
1207 | This is a copy of the `orderless-flex' pattern written without `rx'.
1208 |
1209 | This one may be slower than `fussy-pattern-flex-1' but is more
1210 | exhaustive on matches."
1211 | (list
1212 | (concat
1213 | (when (> (length str) 1)
1214 | "\\(?:\\(?:")
1215 | (mapconcat
1216 | (lambda (x)
1217 | (format "\\(%c\\)" x))
1218 | str
1219 | ".*")
1220 | (when (> (length str) 1)
1221 | "\\)\\)"))))
1222 |
1223 | (defun fussy-pattern-default (str)
1224 | "Make STR flex pattern.
1225 |
1226 | If length if STR is somewhat long, return nil instead as long flex patterns
1227 | can be really slow when filtering."
1228 | (if (> (length str) 4)
1229 | nil
1230 | (fussy-pattern-flex-2 str)))
1231 |
1232 | (defun fussy-pattern-first-letter (str)
1233 | "Make pattern for STR.
1234 |
1235 | str: abc
1236 | result: LIST ^a"
1237 | (if (and str (> (length str) 0))
1238 | `(,(format "^%s" (substring str 0 1)))
1239 | nil))
1240 |
1241 | ;;
1242 | ;; (@* "Integration with other Packages" )
1243 | ;;
1244 |
1245 | ;; `eglot' integration
1246 | ;;;###autoload
1247 | (defun fussy-eglot-setup ()
1248 | "Set up `fussy' with `eglot'."
1249 | (with-eval-after-load 'eglot
1250 | ;; `eglot' defaults to flex, so set an override to point `fussy' instead.
1251 | (add-to-list 'completion-category-overrides
1252 | '(eglot-capf (styles fussy eglot--dumb-flex)))
1253 | (add-to-list 'completion-category-overrides
1254 | '(eglot (styles fussy basic)))))
1255 |
1256 | ;; `company' integration.
1257 | (defvar company-backend)
1258 | (defvar company-prefix)
1259 |
1260 | (defun fussy-company-sort-by-completion-score (candidates)
1261 | "`company' transformer to sort CANDIDATES."
1262 | (if (functionp company-backend)
1263 | candidates
1264 | (fussy--sort candidates)))
1265 |
1266 | (defun fussy-company--transformer (f &rest args)
1267 | "Advise `company--transform-candidates'."
1268 | (if (length< company-prefix fussy-company-prefix-length)
1269 | ;; Transform normally for short prefixes.
1270 | (let ((fussy-can-adjust-metadata-p nil))
1271 | (apply f args))
1272 | (let ((company-transformers
1273 | ;; `fussy-score' still needs to do sorting.
1274 | ;; `fussy-fzf-score' sorts on its own.
1275 | (if (eq fussy-score-ALL-fn 'fussy-score)
1276 | '(fussy-company-sort-by-completion-score)
1277 | '())))
1278 | ;; Warning: Unused lexical variable `company-transformers'
1279 | (ignore company-transformers)
1280 | (apply f args))))
1281 |
1282 | (defun fussy-company--fetch-candidates (f &rest args)
1283 | "Advise `company--fetch-candidates'."
1284 | (let ((prefix (nth 0 args))
1285 | (_suffix (nth 1 args)))
1286 | (if (length< prefix fussy-company-prefix-length)
1287 | (let ((completion-styles (remq 'fussy completion-styles))
1288 | (completion-category-overrides nil)
1289 | (fussy-can-adjust-metadata-p nil))
1290 | (apply f args))
1291 | (let ((fussy-max-candidate-limit 5000)
1292 | (fussy-default-regex-fn 'fussy-pattern-first-letter)
1293 | (fussy-prefer-prefix nil))
1294 | (apply f args)))))
1295 |
1296 | (defun fussy-company--preprocess-candidates (candidates)
1297 | "Advise `company--preprocess-candidates'.
1298 |
1299 | This is to try to avoid a additional sort step."
1300 | ;; (cl-assert (cl-every #'stringp candidates))
1301 | ;; (unless (company-call-backend 'sorted)
1302 | ;; (setq candidates (sort candidates 'string<)))
1303 | (when (and (fboundp 'company-call-backend)
1304 | (fboundp 'company--strip-duplicates))
1305 | (when (company-call-backend 'duplicates)
1306 | (company--strip-duplicates candidates)))
1307 | candidates)
1308 |
1309 | (defun fussy-company-setup ()
1310 | "Set up `company' with `fussy'."
1311 | (with-eval-after-load 'company
1312 | (advice-add 'company-auto-begin :before 'fussy-wipe-cache)
1313 | (advice-add 'company--transform-candidates
1314 | :around 'fussy-company--transformer)
1315 | (advice-add 'company--fetch-candidates
1316 | :around 'fussy-company--fetch-candidates)
1317 | (advice-add 'company--preprocess-candidates
1318 | :override 'fussy-company--preprocess-candidates)))
1319 |
1320 | ;; `fuz' integration.
1321 | (declare-function "fuz-fuzzy-match-skim" "fuz")
1322 | (declare-function "fuz-calc-score-skim" "fuz")
1323 | (declare-function "fuz-fuzzy-match-clangd" "fuz")
1324 | (declare-function "fuz-calc-score-clangd" "fuz")
1325 |
1326 | (defun fussy-flx-rs-score (str query &rest args)
1327 | "Score STR for QUERY with ARGS using `flx-rs-score'."
1328 | (require 'flx-rs)
1329 | (when (fboundp 'flx-rs-score)
1330 | (flx-rs-score (fussy-without-bad-char str) query args)))
1331 |
1332 | (defun fussy-fuz-score (str query &rest _args)
1333 | "Score STR for QUERY using `fuz'.
1334 |
1335 | skim or clangd algorithm can be used.
1336 |
1337 | If `orderless' is used for filtering, we skip calculating matches
1338 | for more speed."
1339 | (require 'fuz)
1340 | (let ((str (fussy-without-bad-char str)))
1341 | (if fussy-fuz-use-skim-p
1342 | (if (fussy--orderless-p)
1343 | (when (fboundp 'fuz-calc-score-skim)
1344 | (list (fuz-calc-score-skim query str)))
1345 | (when (fboundp 'fuz-fuzzy-match-skim)
1346 | (fuz-fuzzy-match-skim query str)))
1347 | (if (fussy--orderless-p)
1348 | (when (fboundp 'fuz-calc-score-clangd)
1349 | (list (fuz-calc-score-clangd query str)))
1350 | (when (fboundp 'fuz-fuzzy-match-clangd)
1351 | (fuz-fuzzy-match-clangd query str))))))
1352 |
1353 | ;; `fuz-bin' integration.
1354 | (declare-function "fuz-bin-dyn-score-skim" "fuz-bin")
1355 | (declare-function "fuz-bin-score-skim" "fuz-bin")
1356 | (declare-function "fuz-bin-dyn-score-clangd" "fuz-bin")
1357 | (declare-function "fuz-bin-score-clangd" "fuz-bin")
1358 |
1359 | (defun fussy-fuz-bin-score (str query &rest _args)
1360 | "Score STR for QUERY using `fuz-bin'.
1361 |
1362 | skim or clangd algorithm can be used.
1363 |
1364 | If `orderless' is used for filtering, we skip calculating matches
1365 | for more speed."
1366 | (require 'fuz-bin)
1367 | ;; (message (format "before: str: %s query: %s" str query))
1368 | (let ((str (fussy-without-bad-char str)))
1369 | ;; (message (format "after: str: %s query: %s" str query))
1370 | (if fussy-fuz-use-skim-p
1371 | (if (fussy--orderless-p)
1372 | (when (fboundp 'fuz-bin-dyn-score-skim)
1373 | (list (fuz-bin-dyn-score-skim query str)))
1374 | (when (fboundp 'fuz-bin-score-skim)
1375 | (fuz-bin-score-skim query str)))
1376 | (if (fussy--orderless-p)
1377 | (when (fboundp 'fuz-bin-dyn-score-clangd)
1378 | (list (fuz-bin-dyn-score-clangd query str)))
1379 | (when (fboundp 'fuz-bin-score-clangd)
1380 | (fuz-bin-score-clangd query str))))))
1381 |
1382 | ;; `liquidmetal' integration
1383 | (declare-function "liquidmetal-score" "liquidmetal")
1384 |
1385 | (defun fussy-liquidmetal-score (str query &rest _args)
1386 | "Score STR for QUERY using `liquidmetal'.
1387 |
1388 | This should be paired with `fussy-filter-orderless' to obtain match
1389 | highlighting."
1390 | (require 'liquidmetal)
1391 | (when (fboundp 'liquidmetal-score)
1392 | (list (liquidmetal-score (fussy-without-bad-char str) query))))
1393 |
1394 | ;; `sublime-fuzzy' integration
1395 | (declare-function "sublime-fuzzy-score" "sublime-fuzzy")
1396 |
1397 | (defun fussy-sublime-fuzzy-score (str query &rest _args)
1398 | "Score STR for QUERY using `sublime-fuzzy'."
1399 | (require 'sublime-fuzzy)
1400 | (when (fboundp 'sublime-fuzzy-score)
1401 | (list (sublime-fuzzy-score query (fussy-without-bad-char str)))))
1402 |
1403 | ;; `fzf-native' integration
1404 | (defvar fussy--fzf-native-slab nil)
1405 | (defsubst fussy--fzf-native-slab ()
1406 | "Return lazy loaded slab for `fzf-native'."
1407 | (or fussy--fzf-native-slab
1408 | (when (fboundp 'fzf-native-make-default-slab)
1409 | (setf fussy--fzf-native-slab (fzf-native-make-default-slab)))))
1410 |
1411 | (defun fussy-fzf-native-score (str query &rest _args)
1412 | "Score STR for QUERY using `fzf-native'."
1413 | (require 'fzf-native)
1414 | (when (fboundp 'fzf-native-score)
1415 | (fzf-native-score
1416 | (fussy-without-bad-char str) query (fussy--fzf-native-slab))))
1417 |
1418 | ;; `hotfuzz' integration
1419 | (declare-function "hotfuzz--cost" "hotfuzz")
1420 |
1421 | (defun fussy-hotfuzz-score (str query &rest _args)
1422 | "Score STR for QUERY using `hotfuzz'."
1423 | (require 'hotfuzz)
1424 | (when (fboundp 'hotfuzz--cost)
1425 | ;; Looks like the score is flipped for `hotfuzz'.
1426 | ;; See `hotfuzz-all-completions'.
1427 | (list (+ 10000 (- (hotfuzz--cost query str))))))
1428 |
1429 | (provide 'fussy)
1430 | ;;; fussy.el ends here
1431 |
--------------------------------------------------------------------------------
/screenshots/fussy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jojojames/fussy/4b4d26661e460cc6351a81b8186d3f7c29c64453/screenshots/fussy.png
--------------------------------------------------------------------------------