├── .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 --------------------------------------------------------------------------------