├── .gitignore ├── .travis.yml ├── Cask ├── LICENSE ├── README.md ├── dev-notes-concurrent.org ├── dev-notes.org ├── sallet-ag.el ├── sallet-autobookmarks.el ├── sallet-bookmarks.el ├── sallet-buffer.el ├── sallet-concurrent.el ├── sallet-core.el ├── sallet-faces.el ├── sallet-filters.el ├── sallet-imenu.el ├── sallet-man.el ├── sallet-occur.el ├── sallet-pkg.el ├── sallet-projectile.el ├── sallet-recentf.el ├── sallet-registers.el ├── sallet-source.el ├── sallet-state.el ├── sallet.el └── tests └── sallet-start-process-test.el /.gitignore: -------------------------------------------------------------------------------- 1 | /.cask/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: emacs-lisp 2 | sudo: false 3 | cache: 4 | directories: 5 | - $TRAVIS_BUILD_DIR/.cask 6 | env: 7 | - EVM_EMACS=emacs-24.3-travis 8 | - EVM_EMACS=emacs-24.4-travis 9 | - EVM_EMACS=emacs-24.5-travis 10 | before_install: 11 | - curl -fsSkL https://gist.github.com/rejeep/ebcd57c3af83b049833b/raw > travis.sh && source ./travis.sh 12 | - evm install $EVM_EMACS --use --skip 13 | - cask 14 | script: 15 | - cask exec buttercup -L . -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa-stable) 3 | (source melpa) 4 | 5 | (package "sallet" "0.0.1" 6 | "Light spherical helmet.") 7 | 8 | (depends-on "dash" "2.10.0") 9 | (depends-on "s" "1.9.0") 10 | (depends-on "f" "0.18.2") 11 | (depends-on "flx" "0.4") 12 | (depends-on "async" "1.2") 13 | (depends-on "shut-up" "0.3.2") 14 | (depends-on "ov" "1.0") 15 | (depends-on "cl-lib" "0.3") 16 | (depends-on "deferred" "0.5.1") 17 | (depends-on "projectile") 18 | 19 | (development 20 | (depends-on "buttercup")) 21 | -------------------------------------------------------------------------------- /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 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 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 | {project} Copyright (C) {year} {fullname} 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sallet [![Build Status](https://travis-ci.org/Fuco1/sallet.svg?branch=master)](https://travis-ci.org/Fuco1/sallet) 2 | 3 | > A type of light spherical helmet. 4 | 5 | # For contributors 6 | 7 | If you want to contribute, read the [dev notes](https://github.com/Fuco1/sallet/blob/master/dev-notes.org) to learn about the architecture. It is modular and extensible, so you can easily add useful bits without breaking the rest. 8 | -------------------------------------------------------------------------------- /dev-notes-concurrent.org: -------------------------------------------------------------------------------- 1 | * Concurrent sallet 2 | 3 | The second evolution of sallet with concurrency built-in for everything. This needs a little bit of architecture overhaul, especially what concerns generating candidates the rest of the pipeline 4 | 5 | * Basic overview 6 | 7 | The main principle behind the operation of this package is cooperative timesharing. The candidates are generated and processed in stages and each stage is only allowed to run for a limited amount of time (usually 10ms) before yielding back to the input loop. This ensures that the user input is picked up in "real time" (almost nobody can notice 10ms delay in typing) but in case there are no events in the Emacs input queue the processing immediately continues. 8 | 9 | This principle adds a bit of boilerplate and makes it a little bit more complicated to write the functions (really coroutines) for each stage in a manner conforming to this convention. We provide some simple to use wrappers to make "cooperative buffered yielding functions" out of regular functions (see for example =csallet-make-buffered-stage=). 10 | 11 | The stages do not need to process all the candidates at once; they emit all they can process in the allocated time and then wait to be called again to continue. They should therefore maintain all the internal state to be able to be restarted. Closures are an excellent tool for this. 12 | 13 | * Pipeline 14 | 15 | All the functions of the pipeline follow the same general interface. The functions in the pipeline are collectively called /stages/. On the input they take a list of =candidates= to process and =pipeline-data= (a plist which can thread arbitrary data through the thread) and on the output they return a plist with following keys (all are optional): 16 | 17 | - =:candidates= is a list of output candidates which are fed to the next stage of the pipeline. In case the key is not present no candidates are passed to the next stage. 18 | - =:finished= is a boolean flag indicating if this stage is finished with processing, i.e. there are no buffered candidates waiting to be processed. Defaults to =t=. 19 | - =:pipeline-data= is a plist of additional data which are to be threaded through the pipeline. This is the conventional way for the stages to communicate with each other. Later stages can add new keys or overwrite the old ones. 20 | 21 | We call the pipeline repeatedly in a loop until all the stages signal that they are done. Each stage MAY run at different speed and process different amount of candidates at each iteration. The stages themselves are responsible for buffering the candidates they could not process in the allocated time. 22 | 23 | Each stage MUST be able to be called repeatedly. 24 | 25 | The stages in the pipeline are the following: 26 | 27 | : generator -> matcher -> indexer -> updater 28 | 29 | The candidates flow from the generator to the updater and are updated or enriched on the way. 30 | 31 | ** Generator 32 | 33 | Generators generate candidates. It does not matter if they are synchronous or asynchronous or come from external processes etc. Each generator is itself responsible for managing its own state. There are in principle two kinds of generators: 34 | 35 | 1. Return all the data in one call 36 | 2. Generate the data over time 37 | 38 | In the first case we can usually write a simple function with no arguments (remember that we can close over current environment if we need some aditional inputs!). This funciton is fully synchronous and returns either a precomputed list or is just so fast that there is no point in making it cooperative. For an example of such a function look at =sallet-buffer-candidates= which comes from the regular synchronou sallet. We can turn such a function into a CSallet compatible generator by wrapping it with =csallet-make-cached-generator=. 39 | 40 | The second kind emits candidates continously in multiple iterations. A typical example is =csallet-occur-generator= which scans the current buffer for lines matching a pattern. If the buffer is very large it might not be able to scan all of it at once. It saves the point from which to continue internally and waits to be restarted to produce more candidates. Once the end of buffer is reached it produces no more candidates and signals being finished by setting the =:finished= field to =t=. 41 | 42 | ** Matcher 43 | ** Sorter 44 | ** Updater 45 | 46 | * The CSallet monad (optional reading) 47 | 48 | All the stages of the pipeline run inside something akin to a monad. In Pseudo-Haskell we can express it as: 49 | 50 | #+BEGIN_SRC haskell 51 | data CSallet a = CSallet { 52 | candidates :: a 53 | , finished :: Bool 54 | , pipelineData :: Map String Anything} 55 | 56 | instance Monad CSallet where 57 | -- [a] -> CSallet [a] 58 | return candidates = CSallet candidates True Map.empty 59 | -- CSallet [c] -> ([c] -> CSallet [p]) -> CSallet [p] 60 | (CSallet candidates finished pipelineData) >>= f = 61 | let CSallet candidates' finished' pipelineData' = (f candidates pipelineData) 62 | in CSallet candidates' (and finished finished') (Map.union pipelineData' pipelineData) 63 | #+END_SRC 64 | 65 | This is implemented in =csallet-bind-processor=. 66 | 67 | * Things we need to figure out and abstract 68 | 69 | ** Generators from processes 70 | 71 | When we start the process it will put output to its output buffer 72 | which csallet will scan and generate candidates from. When the prompt 73 | changes we might need to restart the process but /we also might nod 74 | need to restart/. The mechanism should work such that in case of no 75 | need for process restart we only "reset" the candidates creator to 76 | scan the output buffer from the beginning again (because some 77 | additional filters might have changed). Therefore we need to separate 78 | these actions: 79 | 80 | - process creator :: A function which starts the process and returns a 81 | handle. Should be responsible for prompt parsing/interpretation? 82 | - process restart predicate :: A function which decides if we need to 83 | restart the process based on the input provided. 84 | - candidate creator :: Function which creates candidates from the 85 | process output. This should be a stage and should be ideally 86 | handled most generically so tha we only need to plug in the above 87 | two. 88 | -------------------------------------------------------------------------------- /dev-notes.org: -------------------------------------------------------------------------------- 1 | * Basic terminology 2 | ** Candidate 3 | A candidate is some value which is ultimately returned to the user as 4 | the selected item. A candidate can be any scalar (number, string, 5 | vector, ...) or a list of values. 6 | 7 | ** Prompt 8 | The text that user input into the minibuffer. 9 | 10 | ** Candidates 11 | Vector of all available [[*Candidate][candidates]]. 12 | 13 | ** Candidate index 14 | Index of a particular candidate in the [[*Candidates][candidates]] vector. 15 | 16 | Instead of only being a number, it can be a cons or list where the 17 | ~car~ is the index and the rest is arbitrary user data. 18 | 19 | ** Processed candidates 20 | A list of [[*Candidate%20index][candidate indices]] which are eligible for further processing 21 | (display, selection...). Instead of modifying the candidate list 22 | (which can possibly be static for the entire session) with each input 23 | change, we only change the list of indices. 24 | 25 | For example, if we changed the candidate list itself, on cleaning up 26 | the prompt we would need to recompute the candidate list. 27 | 28 | This list is passed through the [[*The%20pipeline][pipeline]]. 29 | 30 | * The pipeline 31 | 32 | The main pipeline consists of these steps: 33 | 34 | 1. Generator or candidate list (exactly one of these must be provided) 35 | 2. Matcher 36 | 3. Sorter 37 | 4. Renderer 38 | 39 | Next we describe the inputs and outputs of each stage. 40 | 41 | ** Candidates 42 | Candidates can be a list or vector of candidates (static), or a 43 | function taking zero arguments which generates a new list or vector of 44 | candidates for each session (can depend on current environment). 45 | 46 | When given/created, lists are coerced into vectors. 47 | 48 | ** Generator 49 | Generator is responsible for generating new set of possible 50 | /candidates/ with each change of the user input (prompt). Think 51 | /dynamic candidates/. 52 | 53 | It is a function of two arguments: the current source and the current sallet state. 54 | 55 | The return value is a *vector* of candidates, each candidate can be 56 | either a value or a list of values. 57 | 58 | Sallet supports two asynchronous types of sources: 59 | 60 | 1. Using asynchronous IO (async IO or asyncio from now on). 61 | 2. Using =async.el=. 62 | 63 | *** async IO 64 | In this case the generator function returns the process which computes 65 | the candidates. Candidates themselves are updated from the 66 | =process-filter= attached to the process. In case we need to 67 | prematurely stop the process it is enough to kill the process returned 68 | from the generator (available via =sallet-source-get-process=). 69 | 70 | Because the generator has access to the current sallet state and the 71 | source, it can update the candidates dynamically directly on the 72 | source object. 73 | *** async.el 74 | These sources are marked with =async= property set to =t= on the source. 75 | In theory, any synchronous source can be turned into asynchronous one 76 | just by attaching this property. The asynchrony here is that while 77 | computing the candidate list emacs doesn't block. 78 | 79 | This, however, is very experimental so I don't recommend any one 80 | getting into it much 81 | 82 | ** Matcher 83 | Matcher filters the available candidates and produces [[*Processed%20candidates][processed candidates]]. 84 | 85 | It is a function with two input arguments: the vector of candidates 86 | and the current state. 87 | 88 | The output are processed candidates. 89 | 90 | Matchers follow following naming convention: 91 | - if the matcher is a universal matcher (for example, doing subword, 92 | flx or regexp matching) independent of the source, use 93 | ~sallet-matcher-NAME~. 94 | - if the matcher is specific to single source, use ~sallet-SOURCE-NAME-matcher~. 95 | 96 | *** Filters 97 | Most matchers are implemented in terms of simpler reusable filters. 98 | 99 | There is an interface to make these filters composable (they form 100 | something akin to a monad, yay). Therefore, one matcher can compose 101 | these filters acting in different ways on the source pattern (in 102 | sequence, in parallel), can perform set operations on their results 103 | (must match all, must match some) and so on. 104 | 105 | They all have to follow the following interface: 106 | 107 | #+BEGIN_SRC elisp 108 | (defun sallet-filter-NAME (candidates indices pattern) 109 | "CANDIDATES is the vector of candidates. 110 | 111 | INDICES are indices with possibly attached metadata (see 112 | terminology). 113 | 114 | PATTERN is the string pattern we are matching against 115 | candidates. Its semantics are left to the match procedure. 116 | 117 | Returns filtered list of indices which should be included in 118 | further processing.") 119 | #+END_SRC 120 | 121 | It should not be responsibility of a filter to pre-process patterns or 122 | candidates. 123 | 124 | However, there are /composite filters/ which group subfilters together 125 | to provide aggregated functionality (like anding or oring them 126 | together, or making filters operate on parts of the pattern). These 127 | are usually realized as /decorators/ which take a filter and turn it 128 | into another filter by preprocessing any of the parameters before 129 | passing them further. Some examples include 130 | =sallet-make-tokenized-filter= which runs the underlying filter on each 131 | input token separately and then ands the results, or 132 | =sallet-compose-filters-by-pattern= which composes filters by 133 | dispatching different parts of the pattern to different filters. 134 | 135 | *** Predicates 136 | In turn, filters can use /predicates/ to match candidates and update 137 | their index metadata. 138 | 139 | It is important to realize that predicates are completely 140 | independent of sources, candidate vectors, patterns, filters... all 141 | they care about is to get two values to compare and one value to 142 | update, that's it. Filters using predicates can arbitrarily process 143 | the candidate and pattern before passing it in as arguments. 144 | 145 | However, in most cases they should not do it and instead just pass the 146 | values directly, as modification of these values is responsibility of 147 | a pre-processing step, not matching/filtering step. 148 | 149 | In the case where the candidate is a list or a data structure, it is 150 | acceptable to do a projection and pass that into the predicate, but it 151 | has to be realized that this limits the reusability of the filter as 152 | it will only work with that data structure. 153 | 154 | Predicates have this interface: 155 | 156 | #+BEGIN_SRC elisp 157 | (defun sallet-predicate-NAME (candidate index pattern) 158 | "CANDIDATE is the processed candidate. 159 | 160 | INDEX is its associated index and user metadata. 161 | 162 | PATTERN is a pattern we are matching against. 163 | 164 | Returns updated INDEX with optional added metadata or nil if this 165 | candidate should not pass the filter.") 166 | #+END_SRC 167 | 168 | A common implementation of a filter using a predicate is the following: 169 | 170 | #+BEGIN_SRC elisp 171 | (defun sallet-filter- (candidates indices pattern) 172 | "Keep CANDIDATES at INDICES PATTERN." 173 | ;; can be `sallet-candidate-aref' or any other function 174 | ;; called on the result of `sallet-aref' (sallet-candidate-aref is 175 | ;; an automatic projection on `car'). 176 | (--keep (sallet-predicate- ( candidates it) it pattern) indices)) 177 | #+END_SRC 178 | 179 | ** Sorter 180 | Sorter further processes the [[*Processed%20candidates][processed candidates]] by sorting 181 | them---which is the most expected action, but really, arbitrary 182 | transformation is available. 183 | 184 | It is a function with two input arguments: the list of processed candidates 185 | and the current state. 186 | 187 | The output are processed candidates. 188 | 189 | ** Renderer 190 | Renders the candidates in the candidate window. 191 | 192 | It is a function with two mandatory input arguments: a [[*Candidate][candidate]] and the current state. 193 | 194 | Additionally, any extra user data produced by matcher and/or sorter 195 | (that is, the ~cdr~ of this candidate's candidate index) are passed as 196 | an optional third argument. 197 | 198 | * Auxiliary 199 | ** Process creators 200 | A /process creator/ is a function of one argument, prompt, which is 201 | responsible for starting and returning the process which generates 202 | candidates. It does not attach any filters or sentinels on it, it 203 | simply starts it and returns. Other functions are then responsible to 204 | attach filters on this process to add candidates to the source's 205 | candidate vector. 206 | 207 | If you need to "store" information in the process creator (such as 208 | root directory of a search or any other state) you can create a 209 | closure with this information bound. Most sources defined in sallet 210 | use auxiliary functions to create these closures, such as 211 | =sallet-grep-make-process-creator= which takes one argument, the file 212 | name of the file we are grepping, and returns a closure of one 213 | argument, the prompt, with the file name closed over. 214 | 215 | Various decorators exist to change behaviour of these process 216 | creators. First is =sallet-process-creator-first-token-only=, which 217 | only passes the first whitespace-separated token to the decorated 218 | process creator. If the first token hasn't changed the process is 219 | not needlessly rerun. 220 | 221 | This is useful when we want to generate some initial list of 222 | candidates and then further narrow in elisp without the extra overhead 223 | of re-launching possibly slow searches (think about =find(1)= returning 224 | thousands of candidates). 225 | 226 | Another decorator is =sallet-process-creator-min-prompt-length=. 227 | This decorator makes sure to only run the process creator if prompt 228 | length is greater than the specified limit. 229 | 230 | ** Process filters and decorators 231 | Because sallet provides async IO sources and the Emacs async IO is 232 | quite hairy, we provide some auxiliary decorators to deal with output 233 | of processes. 234 | 235 | The most basic is the =sallet-process-filter-line-buffering-decorator= 236 | decorator, which buffers input until it can pass an entire line 237 | further to the underlying process filter. 238 | 239 | Generally, users define new candidates /linewise/ from the output of a 240 | program. We call a function which turns a line of output to a 241 | candidate a /processor/. 242 | 243 | These in themselves can't modify sources as they should be pure 244 | functions. To make defining generators simpler we provide another 245 | auxiliary function =sallet-process-filter-linewise-candidate-decorator= 246 | to turn a processor into a candidate generating process filter. Your 247 | generator than handles the process creation and attaches this process 248 | filter to the process to fill the candidates vector with values. 249 | 250 | Even more high level is the function 251 | =sallet-make-generator-linewise-asyncio= which takes a process creator 252 | (a function of one argument---prompt) and a processor and returns a 253 | /generator/ you can directly assign to your source. This is the way 254 | virtually any linewise async io source can (should) be defined. 255 | -------------------------------------------------------------------------------- /sallet-ag.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-ag.el --- Sallet for ag -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2016 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 13th July 2016 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 's) 29 | 30 | (require 'sallet-source) 31 | 32 | ;; TODO: see `sallet-grep-make-process-creator'. 33 | (defun sallet-ag-make-process-creator (root) 34 | "Return a process creator for gtags-files sallet. 35 | 36 | ROOT is the directory from where we launch ag(1)." 37 | (lambda (prompt) 38 | (with-temp-buffer 39 | (cd root) 40 | (start-process 41 | "ag" nil "ag" 42 | "--nocolor" "--literal" "--line-number" "--smart-case" 43 | "--nogroup" "--column" prompt)))) 44 | 45 | (defun sallet-ag-files-make-process-creator (root) 46 | "Return a process creator for ag-files sallet. 47 | 48 | ROOT is the directory from where we launch ag(1)." 49 | (lambda (prompt) 50 | (with-temp-buffer 51 | (cd root) 52 | (start-process 53 | "ag" nil "ag" "--nocolor" "--literal" 54 | "--smart-case" "-g" prompt)))) 55 | 56 | (defun sallet-ag-processor (input) 57 | (-let (((file line column content) (s-split-up-to ":" input 3))) 58 | (list content file line column))) 59 | 60 | (defun sallet-filter-ag-path-flx (candidates indices pattern) 61 | "Keep ag CANDIDATES flx-matching PATTERN against file path." 62 | (--keep (sallet-predicate-path-flx (cadr (sallet-aref candidates it)) it pattern) indices)) 63 | 64 | (defun sallet-ag-matcher (candidates state) 65 | (let* ((prompt (sallet-state-get-prompt state)) 66 | (indices (sallet-make-candidate-indices candidates))) 67 | (sallet-compose-filters-by-pattern 68 | '(("\\`/\\(.*\\)" 1 sallet-filter-ag-path-flx) 69 | ;; TODO: each sallet should somehow specify if it is doing 70 | ;; "smart case" and if so, wrap the calls to these with 71 | ;; `case-fold-search'. See for example `sallet-ag'. We should 72 | ;; only do the wrap once somewhere high-up to not kill 73 | ;; performance (see the `case-fold-search' caching in 74 | ;; smartparens `sp--with-case-sensitive' for an example). 75 | (t sallet-filter-substring)) 76 | candidates 77 | indices 78 | prompt))) 79 | 80 | ;; TODO: match only on content, add / matcher for path. We should 81 | ;; acomplish this by generating better candidates, not just lines 82 | ;; (identity) 83 | (sallet-defsource ag (asyncio) 84 | "Grep." 85 | (generator 86 | (lambda (source state) 87 | (funcall 88 | (sallet-make-generator-linewise-asyncio 89 | (sallet-process-creator-first-token-only 90 | (sallet-ag-make-process-creator (oref source search-root))) 91 | 'sallet-ag-processor) 92 | source state))) 93 | (search-root) 94 | (init 'sallet--set-search-root) 95 | (before-candidate-render-hook 96 | (eval '(let ((old-file "") 97 | (last-index 999999)) 98 | (-lambda ((_ file) state index) 99 | (when (<= (sallet-car-maybe index) last-index) 100 | (setq old-file "")) 101 | (unless (equal old-file file) 102 | (setq old-file file) 103 | (insert (format 104 | "%s\n" 105 | (sallet-fontify-flx-matches 106 | (plist-get (cdr-safe index) :flx-matches-path) 107 | (propertize 108 | file 'face 'sallet-buffer-default-directory))))) 109 | (setq last-index (sallet-car-maybe index)))) 110 | t)) 111 | (renderer (-lambda ((content _ line column) _ user-data) 112 | ;; TODO: fontify the line/column with different colors 113 | ;; (set up our own faces for that, something like 114 | ;; sallet-grep-line and sallet-grep-column... we will 115 | ;; use these for all the "grep"-like sallets) 116 | (format "%s:%s:%s" 117 | line column 118 | (sallet-fontify-regexp-matches 119 | (plist-get user-data :regexp-matches) 120 | content)))) 121 | (matcher sallet-ag-matcher) 122 | (action (-lambda (source (_ file line column)) 123 | (find-file (concat (oref source search-root) file)) 124 | (widen) 125 | (goto-char (point-min)) 126 | (forward-line (1- (string-to-number line))) 127 | (forward-char (1- (string-to-number column)))))) 128 | 129 | ;; TODO: add a mechanism to initialize arguments through `interactive' 130 | ;; and/or direct parameters. `init' should have an interactive spec 131 | ;; and we should export a constructor. Usage: run ag-files in current 132 | ;; directory instead of asking the user for the root (for 133 | ;; `sallet-buffer') 134 | (sallet-defsource ag-files (asyncio) 135 | "Grep." 136 | (generator 137 | ;; TODO: this is the exact same as ag except for the creator and 138 | ;; processor. Add a common wrapper? 139 | (lambda (source state) 140 | (funcall 141 | (sallet-make-generator-linewise-asyncio 142 | (sallet-process-creator-first-token-only 143 | (sallet-ag-files-make-process-creator (oref source search-root))) 144 | 'identity) 145 | source state))) 146 | (search-root) 147 | (init 'sallet--set-search-root) 148 | (renderer (lambda (candidate _ user-data) 149 | (sallet-fontify-regexp-matches 150 | (plist-get user-data :regexp-matches) 151 | candidate))) 152 | (action (lambda (source file) 153 | (find-file (concat (oref source search-root) file))))) 154 | 155 | (provide 'sallet-ag) 156 | ;;; sallet-ag.el ends here 157 | -------------------------------------------------------------------------------- /sallet-autobookmarks.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-autobookmarks.el --- Autobookmarks sallet -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | (require 'autobookmarks nil t) 30 | 31 | (require 'sallet-source) 32 | (require 'sallet-state) 33 | (require 'sallet-filters) 34 | (require 'sallet-faces) 35 | 36 | (defun sallet-filter-autobookmark-path-substring (candidates indices pattern) 37 | "Keep autobookmark CANDIDATES substring-matching PATTERN against file path." 38 | (let ((quoted-pattern (regexp-quote pattern))) 39 | (--keep (sallet-predicate-path-regexp (cadr (sallet-aref candidates it)) it quoted-pattern) indices))) 40 | 41 | (defun sallet-filter-autobookmark-path-flx (candidates indices pattern) 42 | "Keep autobookmark CANDIDATES flx-matching PATTERN against file path." 43 | (--keep (sallet-predicate-path-flx (cadr (sallet-aref candidates it)) it pattern) indices)) 44 | 45 | (defun sallet-filter-autobookmark-mode-flx (candidates indices pattern) 46 | "Keep autobookmark CANDIDATES flx-matching PATTERN against the major-mode they would open in." 47 | (--keep (sallet-predicate-buffer-major-mode 48 | (-let* (((_ _ . (&alist 'abm-auto-major-mode mm)) (sallet-aref candidates it))) mm) 49 | it pattern) indices)) 50 | 51 | (defun sallet-autobookmarks-matcher (candidates state) 52 | "Match autobookmark CANDIDATES using special rules. 53 | 54 | First, the prompt is split on whitespace. This creates a list of 55 | patterns. 56 | 57 | A pattern starting with / flx-matches against the path to the 58 | file bookmark represents. 59 | 60 | A pattern starting with // substring-matches against the path to the 61 | file bookmark represents. 62 | 63 | A pattern starting with * flx-matches against the major mode the 64 | bookmark would open in. This is guessed using `auto-mode-alist'. 65 | 66 | Any other non-prefixed pattern is matched using the following rules: 67 | 68 | - If the pattern is first of this type at the prompt, it is 69 | flx-matched against the bookmark name. 70 | - All the following patterns are substring matched against the 71 | bookmark name." 72 | (let* ((prompt (sallet-state-get-prompt state)) 73 | (indices (sallet-make-candidate-indices candidates))) 74 | (sallet-compose-filters-by-pattern 75 | '(("\\`//\\(.*\\)" 1 sallet-filter-autobookmark-path-substring) 76 | ("\\`/\\(.*\\)" 1 sallet-filter-autobookmark-path-flx) 77 | ("\\`\\*\\(.*\\)" 1 sallet-filter-autobookmark-mode-flx) 78 | (t sallet-filter-flx-then-substring)) 79 | candidates 80 | indices 81 | prompt))) 82 | 83 | ;; TODO: improve 84 | (defun sallet-autobookmarks-renderer (candidate _state user-data) 85 | "Render an `autobookmarks-mode' CANDIDATE." 86 | (-let* (((name path . data) candidate) 87 | ((&alist 'visits visits) data)) 88 | (format "%-55s%5s %s" 89 | (sallet-compose-fontifiers 90 | ;; TODO: create a "fontify flx after regexp" function to 91 | ;; simplify this common pattern 92 | (propertize name 'face 'sallet-recentf-buffer-name) user-data 93 | '(sallet-fontify-regexp-matches . :regexp-matches) 94 | '(sallet-fontify-flx-matches . :flx-matches)) 95 | (propertize (if visits (int-to-string visits) "0") 'face 'sallet-buffer-size) 96 | (abbreviate-file-name 97 | (sallet-compose-fontifiers 98 | (propertize path 'face 'sallet-recentf-file-path) user-data 99 | '(sallet-fontify-regexp-matches . :regexp-matches-path) 100 | '(sallet-fontify-flx-matches . :flx-matches-path)))))) 101 | 102 | 103 | (defvar sallet-autobookmarks--name-to-major-mode-cache (make-hash-table :test 'equal) 104 | "Name-to-major-mode cache.") 105 | 106 | (defun sallet-autobookmarks--name-to-major-mode (name) 107 | "Return `major-mode' in which file with NAME would open." 108 | (-if-let (mm (gethash name sallet-autobookmarks--name-to-major-mode-cache)) mm 109 | (puthash name 110 | (cond 111 | ((string-match-p "/\\'" name) 112 | "dired-mode") 113 | (t (catch 'match 114 | (--each auto-mode-alist 115 | (when (string-match-p (car it) name) 116 | (throw 'match (symbol-name 117 | (if (listp (cdr it)) 118 | (-last-item (cdr it)) 119 | (cdr it))))))))) 120 | sallet-autobookmarks--name-to-major-mode-cache))) 121 | 122 | (defun sallet-autobookmarks--candidates-comparator (a b) 123 | (-let (((_ _ . (&alist 'time a)) a) 124 | ((_ _ . (&alist 'time b)) b)) 125 | (time-less-p b a))) 126 | 127 | (defun sallet-autobookmarks--uniquify (candidates) 128 | (let* ((duplicates (--select (> (length (cdr it)) 1) 129 | (-group-by 'car candidates))) 130 | (duplicates-paths 131 | (-mapcat (-lambda ((key . group)) 132 | (f-uniquify-alist 133 | (--map (f-slash 134 | (f-expand 135 | (cdr (assoc 'filename (cdr it))))) 136 | group))) 137 | duplicates))) 138 | (-map (-lambda ((row &as name . bookmark)) 139 | (-if-let ((_ . uniquified) 140 | (assoc (f-slash 141 | (f-expand 142 | (cdr (assoc 'filename bookmark)))) 143 | duplicates-paths)) 144 | (cons uniquified bookmark) 145 | row)) 146 | candidates))) 147 | 148 | (defun sallet-autobookmarks--candidate-creator (bookmark) 149 | (-when-let (name 150 | (cond 151 | ((assoc 'filename (cdr bookmark)) 152 | (f-filename 153 | (cdr (assoc 'filename (cdr bookmark))))) 154 | ((assoc 'defaults (cdr bookmark)) 155 | (cadr (assoc 'defaults (cdr bookmark)))))) 156 | (when (string-match-p "/\\'" (car bookmark)) 157 | (setq name (concat name "/"))) 158 | (-snoc (cons name bookmark) 159 | (cons 160 | 'abm-auto-major-mode 161 | (sallet-autobookmarks--name-to-major-mode name))))) 162 | 163 | (defun sallet-autobookmarks-candidates () 164 | (sallet-autobookmarks--uniquify 165 | (-sort 'sallet-autobookmarks--candidates-comparator 166 | (-keep 'sallet-autobookmarks--candidate-creator 167 | (abm-recent-buffers))))) 168 | 169 | (sallet-defsource autobookmarks nil 170 | "Files saved with `autobookmarks-mode'." 171 | (candidates sallet-autobookmarks-candidates) 172 | (matcher sallet-autobookmarks-matcher) 173 | (renderer sallet-autobookmarks-renderer) 174 | (action (-lambda (_source (_ . x)) (abm-restore-killed-buffer x))) 175 | (header "Autobookmarks")) 176 | 177 | (provide 'sallet-autobookmarks) 178 | ;;; sallet-autobookmarks.el ends here 179 | -------------------------------------------------------------------------------- /sallet-bookmarks.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-bookmarks.el --- Bookmarks sallet -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | (require 'pcase) 30 | 31 | (require 'sallet-source) 32 | (require 'sallet-filters) 33 | 34 | (require 'sallet-recentf) 35 | 36 | 37 | (defun sallet--bookmarks-handler-to-desc (handler) 38 | (pcase handler 39 | (`elfeed-search-bookmark-handler "Elfeed search") 40 | (`Info-bookmark-jump "Info") 41 | (`bmkp-jump-dired "Dired") 42 | (`eshell-bookmark--restore "Eshell") 43 | (_ "File"))) 44 | 45 | (defun sallet--bookmarks-get-face (type) 46 | (pcase type 47 | ("Elfeed search" 'sallet-buffer-special) 48 | ("Info" 'sallet-buffer-help) 49 | ("Dired" 'sallet-buffer-directory) 50 | ("File" 'sallet-buffer-ordinary))) 51 | 52 | (defun sallet-bookmarks-candidates () 53 | (bookmark-maybe-load-default-file) 54 | (-map 55 | (lambda (b) 56 | (list 57 | (car b) 58 | (sallet--bookmarks-handler-to-desc (cdr (assq 'handler (cdr b)))) 59 | (or (cdr (assq 'filename (cdr b))) ""))) 60 | bookmark-alist)) 61 | 62 | (defun sallet-bookmarks-renderer (data _ user-data) 63 | (-let (((name type file) data)) 64 | (format "%-45s%15s %s" 65 | (sallet-fontify-flx-matches 66 | (plist-get user-data :flx-matches) 67 | (propertize name 'face 68 | (sallet--bookmarks-get-face type))) 69 | type 70 | (propertize file 'face 'sallet-buffer-default-directory)))) 71 | 72 | (sallet-defsource bookmarks nil 73 | (candidates sallet-bookmarks-candidates) 74 | (matcher sallet-matcher-flx) 75 | (renderer sallet-bookmarks-renderer) 76 | (action (-lambda (_ (name)) 77 | (bookmark-jump name 'switch-to-buffer))) 78 | (header "Bookmarks")) 79 | 80 | ;; TODO: this depends on bookmark+ (`bmkp-file-alist-only', 81 | ;; `bmkp-jump-1'), should probably be moved to a different file. 82 | (sallet-defsource bookmarks-file-only nil 83 | "Bookmarks source, files only." 84 | (candidates (lambda () (--map 85 | (cons 86 | (substring-no-properties (car it)) 87 | (cdr (assoc 'filename (cdr it)))) 88 | (bmkp-file-alist-only)))) 89 | ;; TODO: enable matching on paths with / 90 | (matcher sallet-matcher-flx) 91 | ;; TODO: extract into generic "flx fontify string candidate" 92 | ;; renderer 93 | (renderer sallet-recentf-renderer ;; TODO: directory bookmarks should have different color 94 | ;; (lambda (c _ user-data) 95 | ;; (sallet-fontify-flx-matches 96 | ;; (plist-get user-data :flx-matches) 97 | ;; (car c))) 98 | ) 99 | (action (-lambda (_source (name)) 100 | ;; TODO: doesn't seem to work 101 | (bmkp-jump-1 name 'switch-to-buffer nil))) 102 | (header "Bookmarked files")) 103 | 104 | (sallet-defsource bookmarks-file-only-closed-only (bookmarks-file-only) 105 | "Bookmarks source, files only, closed files only." 106 | (candidates (lambda () (--keep 107 | (let ((path (cdr (assoc 'filename (cdr it))))) 108 | (unless (get-file-buffer path) 109 | (cons (substring-no-properties (car it)) path))) 110 | (bmkp-file-alist-only))))) 111 | 112 | (provide 'sallet-bookmarks) 113 | ;;; sallet-bookmarks.el ends here 114 | -------------------------------------------------------------------------------- /sallet-buffer.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-buffer.el --- Sallet for picking buffers -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 14th September 2015 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | (require 's) 30 | 31 | (require 'ibuffer) 32 | (require 'imenu) 33 | 34 | (require 'sallet-faces) 35 | (require 'sallet-filters) 36 | (require 'sallet-state) 37 | (require 'sallet-source) 38 | 39 | ;; TODO: create a customize group just for sources 40 | (defcustom sallet-buffer-sources '(sallet-source-buffer) 41 | "Sources for `sallet-buffer'. 42 | 43 | Since `sallet' does not make any artificial distinctions between 44 | sources, you can put any source here. However, keeping it 45 | thematic and related to buffers is probably a good idea." 46 | :group 'sallet 47 | :type '(repeat symbol)) 48 | 49 | 50 | ;; Buffer predicates and filters 51 | 52 | (defun sallet-predicate-buffer-imenu (candidate index pattern) 53 | "Check if buffer has an `imenu' item flx-matching pattern. 54 | 55 | CANDIDATE is a buffer or buffer name. 56 | 57 | INDEX is its index and associated meta data. 58 | 59 | PATTERN is a string flx-matched against imenu items. 60 | 61 | Returns updated INDEX with optional added metadata or nil if this 62 | candidate should not pass the filter." 63 | (when (with-current-buffer candidate 64 | (let ((imenu-alist-flat 65 | ;; TODO: cache the flattened 66 | ;; alists so we don't have to 67 | ;; recomute on every inserted letter. 68 | (-flatten (--tree-map (if (stringp it) nil (car it)) 69 | ;; TODO: make sure the alist is initialized 70 | imenu--index-alist)))) 71 | ;; TODO: add list of matching imenu items as metadata so 72 | ;; we can render that somehow in the list? 73 | (--any? (flx-score it pattern) imenu-alist-flat))) 74 | index)) 75 | 76 | (defun sallet-filter-buffer-imenu (candidates indices pattern) 77 | "Keep buffer CANDIDATES at INDICES flx-matching PATTERN against an imenu item." 78 | (--keep (sallet-predicate-buffer-imenu (sallet-candidate-aref candidates it) it pattern) indices)) 79 | 80 | (defun sallet-filter-buffer-major-mode (candidates indices pattern) 81 | "Keep buffer CANDIDATES at INDICES flx-matching PATTERN against current `major-mode'." 82 | (--keep (sallet-predicate-buffer-major-mode 83 | (with-current-buffer (sallet-candidate-aref candidates it) (symbol-name major-mode)) 84 | it pattern) indices)) 85 | 86 | (defun sallet-predicate-buffer-fulltext (candidate index pattern) 87 | "Check if buffer's `buffer-string' regexp-matches pattern. 88 | 89 | CANDIDATE is a buffer or buffer name. 90 | 91 | INDEX is its index and associated meta data. 92 | 93 | PATTERN is a regexp matched against `buffer-string'. 94 | 95 | Returns updated INDEX with optional added metadata or nil if this 96 | candidate should not pass the filter." 97 | (when (with-current-buffer candidate 98 | (save-excursion 99 | (goto-char (point-min)) 100 | (re-search-forward pattern nil t))) 101 | index)) 102 | 103 | (defun sallet-filter-buffer-fulltext (candidates indices pattern) 104 | "Keep buffer CANDIDATES at INDICES regexp-matching PATTERN against `buffer-string'." 105 | (--keep (sallet-predicate-buffer-fulltext (sallet-candidate-aref candidates it) it pattern) indices)) 106 | 107 | (defun sallet-filter-buffer-default-directory-flx (candidates indices pattern) 108 | "Keep buffer CANDIDATES at INDICES flx-matching PATTERN against `default-directory'." 109 | (--keep (sallet-predicate-path-flx 110 | (with-current-buffer (sallet-candidate-aref candidates it) default-directory) 111 | it pattern) indices)) 112 | 113 | (defun sallet-filter-buffer-default-directory-substring (candidates indices pattern) 114 | "Keep buffer CANDIDATES at INDICES substring-matching PATTERN against `default-directory'." 115 | (let ((quoted-pattern (regexp-quote pattern))) 116 | ;; TODO: replace with specialized file substring matcher 117 | (--keep (sallet-predicate-path-regexp 118 | (with-current-buffer (sallet-candidate-aref candidates it) default-directory) 119 | it quoted-pattern) indices))) 120 | 121 | 122 | 123 | (defun sallet-buffer-fontify-buffer-name (candidate) 124 | "Fontify buffer CANDIDATE's name." 125 | (with-current-buffer candidate 126 | (let ((face (cond 127 | ((and (buffer-file-name) 128 | (buffer-modified-p)) 129 | 'sallet-buffer-modified) 130 | ((eq major-mode (quote dired-mode)) 'sallet-buffer-directory) 131 | ((memq major-mode ibuffer-help-buffer-modes) 'sallet-buffer-help) 132 | ((string-match-p "^*" (buffer-name)) 'sallet-buffer-special) 133 | ((and buffer-file-name 134 | (string-match-p ibuffer-compressed-file-name-regexp buffer-file-name)) 135 | 'sallet-buffer-compressed) 136 | (buffer-read-only 'sallet-buffer-read-only) 137 | (t 'sallet-buffer-ordinary)))) 138 | (propertize (buffer-name) 'face face)))) 139 | 140 | (defun sallet-buffer-renderer (candidate _ user-data) 141 | "Render a buffer CANDIDATE." 142 | (with-current-buffer candidate 143 | ;; TODO: make the column widths configurable 144 | (format "%-50s%10s %20s %s" 145 | (truncate-string-to-width 146 | (sallet-compose-fontifiers 147 | candidate user-data 148 | 'sallet-buffer-fontify-buffer-name 149 | '(sallet-fontify-regexp-matches . :regexp-matches) 150 | '(sallet-fontify-flx-matches . :flx-matches)) 151 | 50 nil nil t) 152 | (propertize (file-size-human-readable (buffer-size)) 'face 'sallet-buffer-size) 153 | (truncate-string-to-width 154 | (s-chop-suffix "-mode" 155 | (sallet-fontify-flx-matches 156 | (plist-get user-data :flx-matches-mm) 157 | (symbol-name major-mode))) 158 | 20 nil nil t) 159 | (format (propertize 160 | (concat "(" (or (and (buffer-file-name) (concat "in %s")) 161 | (-when-let (process (get-buffer-process (current-buffer))) 162 | (concat (process-name process) 163 | " run in %s")) 164 | "%s") 165 | ")") 166 | 'face 167 | 'sallet-buffer-default-directory) 168 | (sallet-compose-fontifiers 169 | default-directory user-data 170 | '(sallet-fontify-regexp-matches . :regexp-matches-path) 171 | '(sallet-fontify-flx-matches . :flx-matches-path)))))) 172 | 173 | (defun sallet-buffer-matcher (candidates state) 174 | "Match a buffer candidate using special rules. 175 | 176 | CANDIDATES are buffer names. 177 | 178 | First, the prompt is split on whitespace. This creates a list of 179 | patterns. 180 | 181 | A pattern starting with * is flx-matched against the `major-mode'. 182 | 183 | A pattern starting with @ is flx-matched against the 184 | `imenu--index-alist' entries. These are usually names of 185 | classes, functions, variables defined in the file. 186 | 187 | A pattern starting with # does a full-text regexp search inside 188 | the buffer. 189 | 190 | A pattern starting with / flx-matches against the default directory. 191 | 192 | Any other non-prefixed pattern is matched using the following rules: 193 | 194 | - If the pattern is first of this type at the prompt, it is 195 | flx-matched against the buffer name. 196 | - All the following patterns are substring matched against the 197 | buffer name." 198 | (let* ((prompt (sallet-state-get-prompt state)) 199 | (indices (sallet-make-candidate-indices candidates))) 200 | ;; TODO: add . prefix to match on file extension 201 | ;; TODO: add gtags filter? 202 | (sallet-compose-filters-by-pattern 203 | '(("\\`\\*\\(.*\\)" 1 sallet-filter-buffer-major-mode) 204 | ("\\`@\\(.*\\)" 1 sallet-filter-buffer-imenu) 205 | ("\\`#\\(.*\\)" 1 sallet-filter-buffer-fulltext) 206 | ("\\`//\\(.*\\)" 1 sallet-filter-buffer-default-directory-substring) 207 | ("\\`/\\(.*\\)" 1 sallet-filter-buffer-default-directory-flx) 208 | (t sallet-filter-flx-then-substring)) 209 | candidates 210 | indices 211 | prompt))) 212 | 213 | (defun sallet-buffer-candidates () 214 | (let ((buffers 215 | ;; TODO: preprocess candidates to include 216 | ;; major-mode and directory so we don't have to 217 | ;; query it multiple times (in filtering and 218 | ;; rendering) 219 | (--keep (let ((name (buffer-name it))) 220 | ;; TODO: add a variable where users 221 | ;; can write regexps to exclude 222 | ;; buffers 223 | (unless (string-match-p "^ " name) name)) 224 | (buffer-list)))) 225 | (if (< 1 (length buffers)) 226 | ;; swap the current buffer with the last 227 | ;; recently visited other buffer, so we default 228 | ;; to toggling 229 | (-cons* (cadr buffers) (car buffers) (cddr buffers)) 230 | buffers))) 231 | 232 | ;; TODO: sorting is now done the same way as `buffer-list' returns the 233 | ;; results, in LRU order. We should also try to add some weight to 234 | ;; the flx score. One possibility is to add +100, and decreasing, to 235 | ;; the flx-score for more recent buffers. 236 | (sallet-defsource buffer nil 237 | "Buffer source." 238 | (candidates sallet-buffer-candidates) 239 | (matcher sallet-buffer-matcher) 240 | (action (lambda (_source c) (switch-to-buffer c))) 241 | (header "Buffers") 242 | (renderer sallet-buffer-renderer)) 243 | 244 | (defun sallet-buffer-similar-buffers-candidates (&optional current-buffer) 245 | (let* ((current-name (cond 246 | ((and (featurep 'uniquify) 247 | (if current-buffer 248 | (with-current-buffer current-buffer 249 | (uniquify-buffer-base-name)) 250 | (uniquify-buffer-base-name)))) 251 | ((buffer-name current-buffer)))) 252 | (buffers 253 | (--keep (let ((name (cond 254 | ((and (featurep 'uniquify) 255 | (with-current-buffer it 256 | (uniquify-buffer-base-name)))) 257 | ((buffer-name it))))) 258 | (when (string= name current-name) (buffer-name it))) 259 | (buffer-list)))) 260 | (when (< 1 (length buffers)) 261 | (-cons* (cadr buffers) (car buffers) (cddr buffers))))) 262 | 263 | ;; TODO: remove duplication with `buffer' source 264 | (sallet-defsource similar-buffer (buffer) 265 | "Buffers with the same name but in a different file hierarchy." 266 | (candidates sallet-buffer-similar-buffers-candidates) 267 | (header "Similar buffers")) 268 | 269 | (provide 'sallet-buffer) 270 | ;;; sallet-buffer.el ends here 271 | -------------------------------------------------------------------------------- /sallet-core.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-core.el --- Core shared functions for sallet -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | (require 's) 30 | 31 | (defun sallet-vector-logical-length (vector) 32 | "Return logical length of VECTOR. 33 | 34 | Logical length is the number of non-nil elements from start." 35 | (let ((i 0)) 36 | (catch 'end 37 | (mapc (lambda (x) (if x (setq i (1+ i)) (throw 'end i))) vector) 38 | (length vector)))) 39 | 40 | (defun sallet-make-candidate-indices (candidates) 41 | "Create the indices list for CANDIDATES. 42 | 43 | This is a list from 0 to (1- logical-length-of-candidates). This 44 | list is used in the filtering pipeline and at the end the 45 | remaining indices point to the candidates structure and designate 46 | valid candidates." 47 | (number-sequence 0 (1- (sallet-vector-logical-length candidates)))) 48 | 49 | (defun sallet-car-maybe (cons-or-thing) 50 | "Return `car' of CONS-OR-THING if it is a cons or itself otherwise." 51 | (if (consp cons-or-thing) (car cons-or-thing) cons-or-thing)) 52 | 53 | (defun sallet-list-maybe (cons-or-thing accessor) 54 | "If CONS-OR-THING is not cons, return itself, else call ACCESSOR on it." 55 | (if (consp cons-or-thing) (funcall accessor cons-or-thing) cons-or-thing)) 56 | 57 | (defun sallet-aref (candidates index) 58 | "Return element of CANDIDATES at INDEX. 59 | 60 | If INDEX is a number behaves just like `aref'. 61 | 62 | If INDEX is a cons, take its `car' and then behaves like `aref'." 63 | (if (numberp index) 64 | (aref candidates index) 65 | (aref candidates (car index)))) 66 | 67 | (defun sallet-candidate-aref (candidates index) 68 | "Return candidate from CANDIDATES at INDEX. 69 | 70 | CANDIDATES is a vector of candidates. If the element at index is 71 | a list, return its `car', otherwise return the element without change. 72 | 73 | INDEX is a number, index into the CANDIDATES array. If the index 74 | is a list, take its `car'." 75 | (sallet-car-maybe (aref candidates (sallet-car-maybe index)))) 76 | 77 | (defun sallet-plist-update (plist property data update-function) 78 | "Take PLIST and append DATA to PROPERTY. 79 | 80 | The value at PROPERTY is a list. 81 | 82 | UPDATE-FUNCTION is used to compute the new value inserted into 83 | the plist. It takes two arguments, DATA and old value of 84 | PROPERTY." 85 | (let ((old-data (plist-get plist property))) 86 | (plist-put plist property (funcall update-function data old-data)))) 87 | 88 | (defun sallet-update-index (index &rest properties) 89 | "Update INDEX with PROPERTIES. 90 | 91 | PROPERTIES is a list of properties (PROPERTY NEW-VALUE UPDATE-FUNCTION). 92 | 93 | PROPERTY is the key under which the value is stored. 94 | 95 | NEW-VALUE is the value to combine with the old value. 96 | 97 | UPDATE-FUNCTION is used to compute the new value inserted into 98 | the plist. It takes two arguments, NEW-VALUE and old value of 99 | PROPERTY. 100 | 101 | If UPDATE-FUNCTION is omitted the old value is replaced with NEW-VALUE." 102 | (cons 103 | (sallet-car-maybe index) 104 | (--reduce-from (let* ((property (car it)) 105 | (new-value (cadr it)) 106 | (update-function (or (nth 2 it) (lambda (x _) x)))) 107 | (sallet-plist-update acc property new-value update-function)) 108 | (cdr-safe index) 109 | properties))) 110 | 111 | (defun sallet--xdg-can-open-p (file) 112 | "Return non-nil if FILE can be opened with xdg-open(1)." 113 | (let* ((mime-type (with-temp-buffer 114 | (call-process 115 | "xdg-mime" nil (current-buffer) nil 116 | "query" "filetype" 117 | (expand-file-name file)) 118 | (buffer-string))) 119 | (desktop-file (with-temp-buffer 120 | (call-process 121 | "xdg-mime" nil (current-buffer) nil 122 | "query" "default" (s-trim mime-type)) 123 | (buffer-string)))) 124 | (not (equal "" (s-trim desktop-file))))) 125 | 126 | (defun sallet--find-file-in-emacs-p (file) 127 | "Return non-nil if Emacs should try to open FILE." 128 | (let ((mime-type (with-temp-buffer 129 | (call-process 130 | "file" nil (current-buffer) 131 | nil "-b" "--mime-type" file) 132 | (buffer-string)))) 133 | (string-match-p "^\\(inode\\|text\\)/" mime-type))) 134 | 135 | (provide 'sallet-core) 136 | ;;; sallet-core.el ends here 137 | -------------------------------------------------------------------------------- /sallet-faces.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-faces.el --- Faces for sallet -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Keywords: faces 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | 30 | 31 | ;;; Faces 32 | 33 | (defgroup sallet-faces nil 34 | "Sallet faces." 35 | :group 'sallet) 36 | 37 | (defface sallet-source-header 38 | '((t (:inherit highlight :extend t))) 39 | "Face used to fontify source header." 40 | :group 'sallet-faces) 41 | 42 | (defface sallet-buffer-ordinary 43 | '((t (:inherit font-lock-type-face))) 44 | "Face used to fontify ordinary buffers." 45 | :group 'sallet-faces) 46 | 47 | (defface sallet-buffer-modified 48 | '((t (:inherit font-lock-warning-face))) 49 | "Face used to fontify modified (unsaved) buffers." 50 | :group 'sallet-faces) 51 | 52 | (defface sallet-buffer-compressed 53 | '((t (:inherit font-lock-doc-face))) 54 | "Face used to fontify buffers representing compressed files. 55 | 56 | Compressed files are those matching 57 | `ibuffer-compressed-file-name-regexp'." 58 | :group 'sallet-faces) 59 | 60 | (defface sallet-buffer-read-only 61 | '((t (:inherit font-lock-constant-face))) 62 | "Face used to fontify read-only buffers." 63 | :group 'sallet-faces) 64 | 65 | (defface sallet-buffer-special 66 | '((t (:inherit font-lock-keyword-face))) 67 | "Face used to fontify special buffers. 68 | 69 | Special buffers are those prefixed by *." 70 | :group 'sallet-faces) 71 | 72 | (defface sallet-buffer-help 73 | '((t (:inherit font-lock-comment-face))) 74 | "Face used to fontify help buffers. 75 | 76 | Help buffers are those whose major mode matches 77 | `ibuffer-help-buffer-modes'." 78 | :group 'sallet-faces) 79 | 80 | (defface sallet-buffer-directory 81 | '((t (:inherit font-lock-function-name-face))) 82 | "Face used to fontify directory buffers. 83 | 84 | Directory buffers are those whose major mode is `dired-mode'." 85 | :group 'sallet-faces) 86 | 87 | (defface sallet-buffer-size 88 | '((t (:foreground "RosyBrown"))) 89 | "Face used to fontify buffer size." 90 | :group 'sallet-faces) 91 | 92 | (defface sallet-buffer-default-directory 93 | '((t (:foreground "Sienna3"))) 94 | "Face used to fontify buffer's default directory or process." 95 | :group 'sallet-faces) 96 | 97 | (defface sallet-regexp-match 98 | '((t (:inherit font-lock-variable-name-face :weight bold))) 99 | "Face used to fontify regexp matches." 100 | :group 'sallet-faces) 101 | 102 | (defface sallet-substring-match 103 | '((t (:inherit font-lock-variable-name-face :weight bold))) 104 | "Face used to fontify substring matches." 105 | :group 'sallet-faces) 106 | 107 | (defface sallet-flx-match 108 | '((t (:inherit font-lock-variable-name-face :weight bold 109 | :underline (:color foreground-color :style line)))) 110 | "Face used to fontify flx matches." 111 | :group 'sallet-faces) 112 | 113 | (defface sallet-selection 114 | '((t (:inherit font-lock-variable-name-face :weight bold 115 | :underline (:color foreground-color :style line)))) 116 | "Face used to highlight current selected candidate." 117 | :group 'sallet-faces) 118 | 119 | 120 | ;;; Fontification helpers 121 | 122 | (defun sallet--fontify-regions (regions string face) 123 | "Highlight REGIONS of STRING using FACE. 124 | 125 | REGIONS is a list of conses (BEG . END) where each cons delimits the region. 126 | 127 | STRING is the string we want to fontify." 128 | (let ((new-string (copy-sequence string))) 129 | (-each regions 130 | (-lambda ((beg . end)) 131 | (add-text-properties beg end (list 'face face) new-string))) 132 | new-string)) 133 | 134 | (defun sallet-fontify-regexp-matches (matches string) 135 | "Highlight regexp MATCHES in STRING. 136 | 137 | MATCHES is a list of conses (BEG . END) where each cons delimits 138 | the matched region. 139 | 140 | STRING is the string we want to fontify." 141 | (sallet--fontify-regions matches string 'sallet-regexp-match)) 142 | 143 | (defun sallet-fontify-substring-matches (matches string) 144 | "Highlight substring MATCHES in STRING. 145 | 146 | MATCHES is a list of conses (BEG . END) where each cons delimits 147 | the matched region. 148 | 149 | STRING is the string we want to fontify." 150 | (sallet--fontify-regions matches string 'sallet-substring-match)) 151 | 152 | (defun sallet-fontify-flx-matches (matches string) 153 | "Highlight flx MATCHES in STRING. 154 | 155 | MATCHES is a list of indices where flx matched a letter to the input pattern. 156 | 157 | STRING is the string we want to fontify." 158 | (let ((new-string (copy-sequence string))) 159 | (--each matches 160 | (add-text-properties it (1+ it) (list 'face 'sallet-flx-match) new-string)) 161 | new-string)) 162 | 163 | (defun sallet-compose-fontifiers (string user-data &rest fontifiers) 164 | "Fontify STRING using information from USER-DATA by applying FONTIFIERS. 165 | 166 | FONTIFIERS is an list of (FONTIFIER . ATTRIBUTE) or FONTIFIER. 167 | Fontifiers are applied in sequence. 168 | 169 | ATTRIBUTE is key into the USER-DATA. 170 | 171 | FONTIFIER is a function of one or two arguments. If it has 172 | associated ATTRIBUTE, its value in USER-DATA is passed as first 173 | argument, the string to be fontified as second. Otherwise just 174 | the string is passed to the function." 175 | (--reduce-from (let ((user-value (when (consp it) (plist-get user-data (cdr it)))) 176 | (fn (if (consp it) (car it) it))) 177 | (if (consp it) 178 | (funcall fn user-value acc) 179 | (funcall fn acc))) 180 | string fontifiers)) 181 | 182 | (provide 'sallet-faces) 183 | ;;; sallet-faces.el ends here 184 | -------------------------------------------------------------------------------- /sallet-filters.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-filters.el --- Common predicates, filters and matchers for sallet -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | (require 'flx) 30 | 31 | (require 'sallet-core) 32 | (require 'sallet-state) 33 | 34 | 35 | ;;; Predicates 36 | 37 | (defun sallet--predicate-flx (candidate index pattern matches-property score-property &optional flx-cache) 38 | "Match CANDIDATE at INDEX against PATTERN and update its properties. 39 | 40 | MATCHES-PROPERTY is the name of property where matching positions 41 | of candidate are stored. 42 | 43 | SCORE-PROPERTY is the score of the flx match of this CANDIDATE 44 | against PATTERN. 45 | 46 | FLX-CACHE is a cache maintained by `flx' to reuse heatmaps." 47 | (-when-let (flx-data (flx-score candidate pattern flx-cache)) 48 | (sallet-update-index 49 | index 50 | (list matches-property (cdr flx-data) '-concat) 51 | (list score-property (car flx-data))))) 52 | 53 | (defun sallet-predicate-flx (candidate index pattern) 54 | "Match and score CANDIDATE at INDEX against PATTERN." 55 | (sallet--predicate-flx candidate index pattern :flx-matches :flx-score)) 56 | 57 | (defun sallet-predicate-path-flx (candidate index pattern) 58 | "Match and score path CANDIDATE at INDEX against PATTERN." 59 | (sallet--predicate-flx candidate index pattern :flx-matches-path :flx-score-path flx-file-cache)) 60 | 61 | (defun sallet-predicate-buffer-major-mode (candidate index pattern) 62 | "Match and score CANDIDATE buffer's `major-mode' at INDEX against PATTERN. 63 | 64 | Matching is done using flx alogrithm." 65 | (sallet--predicate-flx candidate index pattern :flx-matches-mm :flx-score-mm)) 66 | 67 | (defun sallet--predicate-regexp (candidate index pattern matches-property) 68 | "Match CANDIDATE at INDEX against PATTERN and update its properties. 69 | 70 | MATCHES-PROPERTY is the name of property where matching positions 71 | of candidate are stored." 72 | (save-match-data 73 | (when (string-match pattern candidate) 74 | (sallet-update-index 75 | index 76 | (list matches-property (cons (match-beginning 0) (match-end 0)) 'cons))))) 77 | 78 | (defun sallet-predicate-regexp (candidate index pattern) 79 | "Match and score CANDIDATE at INDEX against PATTERN." 80 | (sallet--predicate-regexp candidate index pattern :regexp-matches)) 81 | 82 | (defun sallet-predicate-path-regexp (candidate index pattern) 83 | "Match and score path CANDIDATE at INDEX against PATTERN." 84 | (sallet--predicate-regexp candidate index pattern :regexp-matches-path)) 85 | 86 | 87 | ;;; Filters 88 | 89 | ;; TODO: add a filter constructor which will take a predicate, a 90 | ;; candidate preprocessor and return a filter 91 | 92 | ;; TODO: figure out how the caching works 93 | (defun sallet-filter-flx (candidates indices pattern) 94 | "Match CANDIDATES at INDICES against PATTERN. 95 | 96 | CANDIDATES is a vector of candidates. 97 | 98 | INDICES is a list of processed candidates. 99 | 100 | Uses the `flx' algorithm." 101 | (if (equal "" pattern) indices 102 | (--keep (sallet-predicate-flx (sallet-candidate-aref candidates it) it pattern) indices))) 103 | 104 | (defun sallet-filter-path-flx (candidates indices pattern) 105 | "Match path CANDIDATES at INDICES against PATTERN. 106 | 107 | CANDIDATES is a vector of candidates. 108 | 109 | INDICES is a list of processed candidates. 110 | 111 | Uses the `flx' algorithm." 112 | (if (equal "" pattern) indices 113 | (--keep (sallet-predicate-path-flx (sallet-candidate-aref candidates it) it pattern) indices))) 114 | 115 | ;; TODO: this shouldn't be written in terms of regexp matching but 116 | ;; something like flx only that it takes substrigs. So we should 117 | ;; match "more important" parts first and score properly etc. 118 | ;; TODO: make a specialized version for file names 119 | (defun sallet-filter-substring (candidates indices pattern) 120 | "Match CANDIDATES at INDICES against PATTERN. 121 | 122 | CANDIDATES is a vector of candidates. 123 | 124 | INDICES is a list of processed candidates. 125 | 126 | Uses substring matching." 127 | (let ((quoted-pattern (regexp-quote pattern))) 128 | (--keep (sallet-predicate-regexp (sallet-candidate-aref candidates it) it quoted-pattern) indices))) 129 | 130 | (defun sallet-filter-file-extension (candidates indices pattern) 131 | "Match CANDIDATES at INDICES against PATTERN as file extension. 132 | 133 | CANDIDATES is a vector of candidates. 134 | 135 | INDICES is a list of processed candidates." 136 | (let ((quoted-pattern (concat "\\." (regexp-quote pattern) "[^.]*\\'"))) 137 | (--keep (sallet-predicate-regexp (sallet-candidate-aref candidates it) it quoted-pattern) indices))) 138 | 139 | 140 | ;;; Filter combinators 141 | 142 | (defun sallet--filter-flx-then-substring (candidates indices pattern flx-filter flx-score) 143 | "Match CANDIDATES at INDICES against PATTERN with flx- or substring-matching. 144 | 145 | FLX-FILTER is a filter using some flx algorithm, typically with 146 | special preferences (file paths, general strings) for different 147 | kinds of candidates. 148 | 149 | FLX-SCORE is the property on which we decide whether to use flx 150 | or substring maching. 151 | 152 | This is an internal method, for the general logic see 153 | `sallet-filter-flx-then-substring'." 154 | (if (or (not (consp (car indices))) 155 | (not (plist-member (cdar indices) flx-score))) 156 | (funcall flx-filter candidates indices pattern) 157 | (sallet-filter-substring candidates indices (regexp-quote pattern)))) 158 | 159 | (defun sallet-filter-flx-then-substring (candidates indices pattern) 160 | "Match CANDIDATES at INDICES against PATTERN with flx- or substring-matching. 161 | 162 | CANDIDATES are strings. 163 | 164 | We use following check to determine which algorithm to use: 165 | 1. Pick the first index from INDICES. 166 | 2. If it contains metadata related to flx-matching, we substring 167 | match, otherwise flx-matching was never performed so we flx-match." 168 | (sallet--filter-flx-then-substring 169 | candidates indices pattern 170 | 'sallet-filter-flx :flx-score)) 171 | 172 | ;; TODO: we also need to specify the substring match filter, otherwise 173 | ;; it can grab wrong parts of the candidate 174 | (defun sallet-filter-path-flx-then-substring (candidates indices pattern) 175 | "Match path CANDIDATES at INDICES against PATTERN with flx/substring-matching. 176 | 177 | CANDIDATES are strings. 178 | 179 | We use following check to determine which algorithm to use: 180 | 1. Pick the first index from INDICES. 181 | 2. If it contains metadata related to flx-matching, we substring 182 | match, otherwise flx-matching was never performed so we flx-match." 183 | (sallet--filter-flx-then-substring 184 | candidates indices pattern 185 | 'sallet-filter-path-flx :flx-score-path)) 186 | 187 | ;; TODO: turn into a transformer returning a filter closure 188 | (defun sallet-pipe-filters (filters candidates indices pattern) 189 | "Run all FILTERS in sequence, filtering CANDIDATES at INDICES against PATTERN." 190 | (--reduce-from (funcall it candidates acc pattern) indices filters)) 191 | 192 | ;; TODO: maybe we can add support for "... ..." patterns by "spliting 193 | ;; as sexps" in a temporary buffer where we set everything to word 194 | ;; syntax except spaces and quotes. Then each sexp is one token 195 | ;; (symbols converted to strings first) 196 | ;; TODO: Add optimization where we only re-run changed tokens. We can 197 | ;; keep the index from the last update and just work on that 198 | ;; (similarly as we keep the pattern from one update ago). 199 | ;; TODO: turn into a transformer returning a filter closure 200 | (defun sallet-compose-filters-by-pattern (filter-alist candidates indices pattern) 201 | "Compose filters to match tokens based on patterns. 202 | 203 | FILTER-ALIST is an alist of (SUBPATTERN . FILTERS) or (SUBPATTERN 204 | MATCH-GROUP . FILTERS). 205 | 206 | SUBPATTERN is a regular expression, FILTERS is a list of filters, 207 | MATCH-GROUP is a match group of SUBPATTERN. 208 | 209 | First, PATTERN is split on whitespace into a list of TOKENS. 210 | 211 | Then for each TOKEN we find first SUBPATTERN that matches it and 212 | filter INDICES through the associated list of filters. The 213 | pattern passed to the filter is the value of first match group of 214 | SUBPATTERN or MATCH-GROUP if specified. 215 | 216 | The special SUBPATTERN t signifies a default branch. As soon 217 | as this SUBPATTERN is found the search stops and its filters are 218 | applied. 219 | 220 | Return INDICES filtered in this manner by all the TOKENS." 221 | (let* ((input (split-string pattern))) 222 | (-each input 223 | (lambda (token) 224 | (-when-let ((input . filters) 225 | (--some 226 | (let ((subpattern (car it)) 227 | (match-group (if (numberp (cadr it)) (cadr it) 0)) 228 | (filters (if (numberp (cadr it)) (cddr it) (cdr it)))) 229 | (if (eq t subpattern) 230 | (cons token filters) 231 | (when (string-match subpattern token) 232 | (cons (match-string match-group token) filters)))) 233 | filter-alist)) 234 | (unless (equal input "") 235 | (setq indices (sallet-pipe-filters filters candidates indices input)))))) 236 | indices)) 237 | 238 | (defun sallet-make-tokenized-filter (filter) 239 | "Transform FILTER to match each token from input pattern separately. 240 | 241 | Input pattern is split on whitespace to create list of tokens. 242 | Each candidate is then matched against each token. Only 243 | candidates matching all tokens will pass the test." 244 | (lambda (candidates indices pattern) 245 | (let ((tokens (split-string pattern))) 246 | (--reduce-from (funcall filter candidates acc it) indices tokens)))) 247 | 248 | (defun sallet-ignore-first-token-filter (filter) 249 | "Transform FILTER to ignore the first token of prompt. 250 | 251 | Input pattern is split on whitespace to create list of tokens. 252 | The first token is dropped and then the resulting strings are 253 | concatenated again and passed to FILTER. 254 | 255 | This is useful in asyncio sources where we pass the first token 256 | to the asyncio process and the rest to the matcher." 257 | (lambda (candidates indices pattern) 258 | (let* ((tokens (split-string pattern " ")) 259 | (new-pattern (mapconcat 'identity (cdr tokens) " "))) 260 | (if (equal new-pattern "") indices 261 | (funcall filter candidates indices new-pattern))))) 262 | 263 | 264 | ;;; Matchers 265 | 266 | ;; TODO: Go over the matchers and check if they actually need the 267 | ;; entire state. 268 | (defun sallet-matcher-default (candidates state) 269 | "Default matcher. 270 | 271 | Take CANDIDATES, which is a vector of candidates from a source 272 | and a sallet STATE and return a list of indices of matching 273 | candidates. 274 | 275 | The prompt is split on whitespace, then candidate must 276 | substring-match each token to pass the test." 277 | (let ((prompt (sallet-state-get-prompt state)) 278 | (indices (sallet-make-candidate-indices candidates))) 279 | (funcall (sallet-make-tokenized-filter 'sallet-filter-substring) candidates indices prompt))) 280 | 281 | (defun sallet-matcher-flx-then-substring (candidates state) 282 | "Match first token with `flx' and then substring match the rest. 283 | 284 | Take CANDIDATES, which is a vector of candidates from a source 285 | and a sallet STATE and return a list of indices of matching 286 | candidates." 287 | (let ((prompt (sallet-state-get-prompt state)) 288 | (indices (sallet-make-candidate-indices candidates))) 289 | (funcall (sallet-make-tokenized-filter 'sallet-filter-flx-then-substring) candidates indices prompt))) 290 | 291 | ;; TODO: write a "defmatcher" macro which would automatically define 292 | ;; prompt and indices variables 293 | (defun sallet-matcher-flx (candidates state) 294 | "Match candidates using `flx' matching. 295 | 296 | Take CANDIDATES, which is a vector of candidates from a source 297 | and a sallet STATE and return a list of indices of matching 298 | candidates." 299 | (let ((prompt (sallet-state-get-prompt state)) 300 | (indices (sallet-make-candidate-indices candidates))) 301 | (sallet-filter-flx candidates indices prompt))) 302 | 303 | 304 | ;;; Matcher combinators 305 | 306 | ;; TODO: Start replacing built-in manually crafted matchers with calls 307 | ;; to this 308 | (defun sallet-make-matcher (filter) 309 | "Make a sallet matcher from a FILTER." 310 | (lambda (candidates state) 311 | (let ((prompt (sallet-state-get-prompt state)) 312 | (indices (sallet-make-candidate-indices candidates))) 313 | (funcall filter candidates indices prompt)))) 314 | 315 | 316 | ;;; Sorters 317 | 318 | ;; TODO: figure out how to compose this when multiple filters are in 319 | ;; place and not all of them provide the sorting attribute 320 | (defun sallet-sorter-flx (processed-candidates _) 321 | "Sort PROCESSED-CANDIDATES by :flx-score." 322 | (sort processed-candidates 323 | (lambda (a b) 324 | ;; UGLY!!!! 325 | (if (and (consp a) (consp b)) 326 | (-when-let* (((_ &keys :flx-score sa) a) 327 | ((_ &keys :flx-score sb) b)) 328 | (> sa sb)) 329 | (> a b))))) 330 | 331 | (provide 'sallet-filters) 332 | ;;; sallet-filters.el ends here 333 | -------------------------------------------------------------------------------- /sallet-imenu.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-imenu.el --- Imenu sallet -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Package-requires: ((dash "2.10.0")) 10 | ;; Keywords: convenience 11 | 12 | ;; This program is free software; you can redistribute it and/or 13 | ;; modify it under the terms of the GNU General Public License 14 | ;; as published by the Free Software Foundation; either version 3 15 | ;; of the License, or (at your option) any later version. 16 | 17 | ;; This program is distributed in the hope that it will be useful, 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | ;; GNU General Public License for more details. 21 | 22 | ;; You should have received a copy of the GNU General Public License 23 | ;; along with this program. If not, see . 24 | 25 | ;;; Commentary: 26 | 27 | ;;; Code: 28 | 29 | (require 'dash) 30 | 31 | (require 'imenu) 32 | 33 | (require 'sallet-filters) 34 | (require 'sallet-faces) 35 | (require 'sallet-source) 36 | 37 | 38 | (defun sallet--imenu-flatten (alist) 39 | "Flatten an imenu ALIST." 40 | (--mapcat (if (imenu--subalist-p it) 41 | (-map (-lambda ((name pos . tags)) (-cons* name pos (car it) tags)) (sallet--imenu-flatten (cdr it))) 42 | (list (list (car it) (cdr it)))) 43 | alist)) 44 | 45 | (defun sallet-filter-imenu-tags-flx (candidates indices pattern) 46 | "Keep buffer CANDIDATES at INDICES flx-matching PATTERN against imenu tags." 47 | (--keep (sallet-predicate-path-flx 48 | (mapconcat 'identity (cddr (sallet-aref candidates it)) ", ") 49 | it pattern) indices)) 50 | 51 | (defun sallet-imenu-renderer (candidate _state user-data) 52 | "Render an imenu CANDIDATE." 53 | (-let* (((x _ . tags) candidate) 54 | (face (cond ((member "Variables" tags) 55 | 'font-lock-variable-name-face) 56 | ((member "Types" tags) 57 | 'font-lock-type-face) 58 | (t 'font-lock-function-name-face)))) 59 | (format "%-80s%s" 60 | (sallet-compose-fontifiers 61 | (propertize x 'face face) user-data 62 | '(sallet-fontify-regexp-matches . :regexp-matches) 63 | '(sallet-fontify-flx-matches . :flx-matches)) 64 | (sallet-compose-fontifiers 65 | (mapconcat 'identity tags ", ") user-data 66 | '(sallet-fontify-regexp-matches . :regexp-matches-path) 67 | '(sallet-fontify-flx-matches . :flx-matches-path))))) 68 | 69 | (defun sallet-imenu-candidates () 70 | "Compute imenu candidates." 71 | ;; We need to clean the index for `imenu--make-index-alist' to 72 | ;; refresh. 73 | (setq imenu--index-alist nil) 74 | (let ((initial (symbol-name (symbol-at-point))) 75 | (cands (--map (if (cddr it) it (-snoc it "")) 76 | (--remove 77 | (or (not (integer-or-marker-p (cadr it))) 78 | (< (cadr it) 0)) 79 | (sallet--imenu-flatten (imenu--make-index-alist)))))) 80 | (if initial 81 | (-if-let (initial (--first (equal (car it) initial) cands)) 82 | (cons initial (--remove (equal initial it) cands)) 83 | cands) 84 | cands))) 85 | 86 | (sallet-defsource imenu nil 87 | "Imenu." 88 | (candidates sallet-imenu-candidates) 89 | (matcher (sallet-make-matcher 90 | (lambda (c i p) 91 | ;; TODO: maybe just search in name and tags by default 92 | ;; but prioritize matches in the name first 93 | (sallet-compose-filters-by-pattern 94 | '(("\\`/\\(.*\\)" 1 sallet-filter-imenu-tags-flx) 95 | (t sallet-filter-flx-then-substring)) c i p)))) 96 | (sorter sallet-sorter-flx) 97 | (renderer sallet-imenu-renderer) 98 | (action (-lambda (_source (_ pos)) 99 | (cond 100 | ((eq major-mode 'org-mode) 101 | (goto-char pos) 102 | (org-show-context) 103 | (org-show-entry)) 104 | (t (goto-char pos))))) 105 | (header "Imenu")) 106 | 107 | (provide 'sallet-imenu) 108 | ;;; sallet-imenu.el ends here 109 | -------------------------------------------------------------------------------- /sallet-man.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-man.el --- Sallet for man(1) 2 | 3 | ;; Copyright (C) 2016 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 27th October 2016 9 | ;; Package-requires: ((dash "2.10.0")) 10 | ;; Keywords: convenience, help 11 | 12 | ;; This program is free software; you can redistribute it and/or 13 | ;; modify it under the terms of the GNU General Public License 14 | ;; as published by the Free Software Foundation; either version 3 15 | ;; of the License, or (at your option) any later version. 16 | 17 | ;; This program is distributed in the hope that it will be useful, 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | ;; GNU General Public License for more details. 21 | 22 | ;; You should have received a copy of the GNU General Public License 23 | ;; along with this program. If not, see . 24 | 25 | ;;; Commentary: 26 | 27 | ;;; Code: 28 | 29 | (require 'sallet-source) 30 | 31 | (sallet-defsource man (asyncio) 32 | (generator 33 | (sallet-make-generator-linewise-asyncio 34 | (sallet-process-creator-first-token-only 35 | (lambda (prompt) 36 | (start-process "man" nil "man" "-k" prompt))) 37 | 'identity)) 38 | (header "man") 39 | (action (lambda (_ c) 40 | (man (car (split-string c " ")))))) 41 | 42 | (provide 'sallet-man) 43 | ;;; sallet-man.el ends here 44 | -------------------------------------------------------------------------------- /sallet-occur.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-occur.el --- Occur sallet -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | 30 | (require 'sallet-source) 31 | 32 | ;; TODO: write docstring 33 | (defun sallet-occur-get-lines (buffer prompt &optional mode no-font-lock) 34 | "Find lines of BUFFER matching PROMPT. 35 | 36 | The optional argument MODE specifies the algorithm used and is 37 | one of: :normal, :fuzzy, :regexp (default). 38 | 39 | The optional argument NO-FONT-LOCK specifies whether we force 40 | font-locking of matched line or not (default on: we font-lock)." 41 | (let ((pattern 42 | (concat "\\(" 43 | (cond 44 | ((eq mode :normal) 45 | (regexp-quote prompt)) 46 | ((eq mode :fuzzy) 47 | (mapconcat 'identity 48 | (mapcar 'char-to-string (string-to-list prompt)) 49 | ".*")) 50 | (t prompt)) 51 | "\\)")) 52 | re) 53 | (with-current-buffer buffer 54 | (goto-char (point-min)) 55 | (while (re-search-forward pattern nil t) 56 | (let* ((lb (line-beginning-position)) 57 | (le (line-end-position)) 58 | (line (save-excursion 59 | (if no-font-lock 60 | (buffer-substring-no-properties lb le) 61 | (font-lock-fontify-region lb le) 62 | (buffer-substring lb le))))) 63 | (push (list line (point) (line-number-at-pos)) re))) 64 | (vconcat (nreverse re))))) 65 | 66 | (sallet-defsource occur nil 67 | "Occur source." 68 | (candidates nil) 69 | (matcher nil) 70 | (renderer (-lambda ((line-string _ line-number) _ _) 71 | ;; TODO: add face to the number 72 | (format "%5d:%s" line-number line-string))) 73 | (generator '(let ((buffer (current-buffer))) 74 | (lambda (_ state) 75 | (let ((prompt (sallet-state-get-prompt state))) 76 | ;; TODO: move this into a separate setting... this 77 | ;; is going to be quite common for "computing" 78 | ;; sources 79 | (when (>= (length prompt) 2) 80 | (sallet-occur-get-lines buffer prompt :normal)))))) 81 | (action (lambda (_source c) 82 | ;; TODO: why isn't it enough to use `goto-char'? Probably 83 | ;; active window is badly re-set after candidate window is 84 | ;; disposed 85 | (set-window-point (selected-window) (cadr c))))) 86 | 87 | (sallet-defsource occur-async (occur) 88 | "Async occur source." 89 | (async t) 90 | (renderer (-lambda ((line-string _ line-number) _ _) 91 | ;; TODO: add face to the number 92 | ;; TODO: fontify the result line here instead of in the async process 93 | (format "%5d:%s" line-number line-string))) 94 | ;; the buffer we search is the current buffer in the async instance 95 | (generator (lambda (_ state) 96 | (let ((prompt (sallet-state-get-prompt state))) 97 | ;; TODO: move this into a separate setting... this 98 | ;; is going to be quite common for "computing" 99 | ;; sources 100 | (when (>= (length prompt) 2) 101 | (sallet-occur-get-lines (current-buffer) prompt :normal :no-font-lock)))))) 102 | 103 | (sallet-defsource occur-fuzzy (occur) 104 | "Fuzzy occur source." 105 | ;; matcher is used to rank & reorder best matches on top ... 106 | (matcher sallet-matcher-flx) 107 | (sorter sallet-sorter-flx) 108 | ;; ... while generator is the stupidest matcher possible 109 | (generator '(let ((buffer (current-buffer))) 110 | (lambda (_ state) 111 | (let ((prompt (sallet-state-get-prompt state))) 112 | (when (>= (length prompt) 2) 113 | (sallet-occur-get-lines buffer prompt :fuzzy))))))) 114 | 115 | 116 | (provide 'sallet-occur) 117 | ;;; sallet-occur.el ends here 118 | -------------------------------------------------------------------------------- /sallet-pkg.el: -------------------------------------------------------------------------------- 1 | (define-package "sallet" "0.0.1" "Light spherical helmet." 2 | '((projectile nil) 3 | (deferred "0.5.1") 4 | (cl-lib "0.3") 5 | (ov "1.0") 6 | (shut-up "0.3.2") 7 | (async "1.2") 8 | (flx "0.4") 9 | (f "0.18.2") 10 | (s "1.9.0") 11 | (dash "2.10.0") 12 | (emacs "25.1"))) 13 | -------------------------------------------------------------------------------- /sallet-projectile.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-projectile.el --- Sallet for projectile -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2016 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 17th September 2016 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | (require 'projectile nil t) 30 | 31 | (require 'sallet-source) 32 | 33 | (sallet-defsource projectile-projects nil 34 | (candidates (lambda () (projectile-relevant-known-projects))) 35 | (matcher sallet-matcher-flx) 36 | (action (lambda (_source c) (projectile-switch-project-by-name c))) 37 | (header "Jump to project")) 38 | 39 | (defun sallet-projectile-projects () 40 | "Switch project." 41 | (interactive) 42 | (sallet (list sallet-source-projectile-projects))) 43 | 44 | 45 | (provide 'sallet-projectile) 46 | ;;; sallet-projectile.el ends here 47 | -------------------------------------------------------------------------------- /sallet-recentf.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-recentf.el --- Recentf sallet -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | 30 | (require 'sallet-source) 31 | (require 'sallet-faces) 32 | 33 | (defface sallet-recentf-buffer-name 34 | '((t (:inherit font-lock-builtin-face))) 35 | "Face used to fontify recentf buffer name." 36 | :group 'sallet-faces) 37 | 38 | (defface sallet-recentf-file-path 39 | '((t (:inherit sallet-buffer-default-directory))) 40 | "Face used to fontify recentf file path." 41 | :group 'sallet-faces) 42 | 43 | ;; TODO: faces should come as optional parameters, this should be called "bookmark cons" renderer 44 | (defun sallet-recentf-renderer (candidate _state user-data) 45 | "Render a recentf CANDIDATE." 46 | (-let (((name . file) candidate)) 47 | (format "%-50s%s" 48 | (sallet-fontify-flx-matches 49 | (plist-get user-data :flx-matches) 50 | (propertize name 'face 'sallet-recentf-buffer-name)) 51 | (propertize (abbreviate-file-name file) 'face 'sallet-recentf-file-path)))) 52 | 53 | (sallet-defsource recentf nil 54 | "Files saved on `recentf-list'." 55 | (candidates (lambda () 56 | (unless recentf-mode (recentf-mode 1)) 57 | (--map 58 | (let ((name (file-name-nondirectory it))) 59 | (cons name it)) 60 | recentf-list))) 61 | ;; TODO: add matching on path with / 62 | (matcher sallet-matcher-flx) 63 | (renderer sallet-recentf-renderer) 64 | (action (-lambda (_source (_ . file)) (find-file file))) 65 | (header "Recently opened files")) 66 | 67 | (sallet-defsource recentf-closed-only (recentf) 68 | "Files saved on `recentf-list', but without those whose buffer is already opened." 69 | (candidates (lambda () 70 | (unless recentf-mode (recentf-mode 1)) 71 | (--keep 72 | (let ((name (file-name-nondirectory it))) 73 | (unless (get-file-buffer it) 74 | (cons name it))) 75 | recentf-list)))) 76 | 77 | (provide 'sallet-recentf) 78 | ;;; sallet-recentf.el ends here 79 | -------------------------------------------------------------------------------- /sallet-registers.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-registers.el --- Register sallet -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2016 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 16th May 2016 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | (require 's) 30 | 31 | (require 'sallet-source) 32 | (require 'sallet-faces) 33 | 34 | (sallet-defsource register-point nil 35 | "Sallet for navigating to point registers. 36 | 37 | Point registers (set by \\[point-to-register]) serve as transient bookmarks. 38 | 39 | Each is identified by the register, which is a number or a letter." 40 | (candidates (lambda () 41 | (--keep 42 | (let* ((marker (cdr it)) 43 | (buffer (marker-buffer marker))) 44 | (when (buffer-live-p buffer) 45 | (with-current-buffer buffer 46 | (save-excursion 47 | (goto-char marker) 48 | (let ((line-number (line-number-at-pos))) 49 | (list (s-trim (thing-at-point 'line)) line-number (buffer-name buffer) it)))))) 50 | (--filter (markerp (cdr it)) register-alist)))) 51 | (matcher sallet-matcher-flx-then-substring) 52 | (renderer (-lambda ((line line-number buffer-name) _ user-data) 53 | (format "% 5d:%-30s%-80s" 54 | line-number 55 | buffer-name 56 | (sallet-compose-fontifiers 57 | line user-data 58 | '(sallet-fontify-regexp-matches . :regexp-matches) 59 | '(sallet-fontify-flx-matches . :flx-matches))))) 60 | (action (-lambda (_source (_ _ _ (register))) 61 | (message "%s" register) 62 | (jump-to-register register))) 63 | (header "Point registers")) 64 | 65 | (provide 'sallet-registers) 66 | ;;; sallet-registers.el ends here 67 | -------------------------------------------------------------------------------- /sallet-source.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-source.el --- Abstract source definition -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | 28 | (require 'dash) 29 | 30 | (require 'eieio) 31 | 32 | (require 'sallet-core) 33 | 34 | ;; TODO: add docs 35 | (defmacro sallet-defsource (name parents &optional docstring &rest body) 36 | (declare (doc-string 3) 37 | (debug (&define name (&rest arg) [&optional stringp] def-body)) 38 | (indent defun)) 39 | (unless (stringp docstring) (setq body (cons docstring body))) 40 | `(defclass ,(intern (concat "sallet-source-" (symbol-name name))) 41 | ,(--map (intern (concat "sallet-source-" (symbol-name it))) 42 | (if (and (not (memq name '(-default default))) 43 | (not parents)) 44 | (list 'default) parents)) 45 | ,(-map (lambda (arg) 46 | (let (re) 47 | (push (list (car arg) :initform (cadr arg)) re) 48 | (when (plist-member arg :documentation) 49 | (push (list :documentation (plist-get arg :documentation)) re)) 50 | (apply '-concat (nreverse re)))) 51 | body) 52 | ,@(when (stringp docstring) (list :documentation docstring)))) 53 | 54 | (font-lock-add-keywords 'emacs-lisp-mode `(("(\\(sallet-defsource\\)\\>[[:blank:]]+\\(.*?\\)[[:blank:]]" 55 | (1 font-lock-keyword-face) 56 | (2 font-lock-type-face)))) 57 | 58 | (sallet-defsource -default () 59 | "The absolute parent of the source hierarchy. 60 | 61 | This is an internal class. 62 | 63 | This source defines internal variables used to hold state during 64 | the picking process." 65 | (processed-candidates nil)) 66 | 67 | ;; TODO: add a simple interface to create an interactive transient 68 | ;; "source" with just a static list of candidates (i.e. support for 69 | ;; anonymous sources) 70 | ;; TODO: remake all the functions to take source as first 71 | ;; argument... this allows us to save state inside the source as with 72 | ;; normal objects. 73 | (sallet-defsource default (-default) 74 | "Default source. 75 | 76 | Every user or package-defined source must inherit from this 77 | source. If the user does not specify any source to inherit from, 78 | this is added automatically! 79 | 80 | Sets default matcher `sallet-matcher-default', identity renderer 81 | and identity action." 82 | (matcher sallet-matcher-default 83 | :documentation "write what a matcher is. function matching and ranking/sorting candidates") 84 | (sorter nil :documentation "Sorter.") 85 | (init ignore 86 | :documentation 87 | "Arbitrary initialization function. 88 | 89 | The function is run in the context of the buffer where sallet was 90 | invoked and takes one argument, the current source.") 91 | (renderer (lambda (candidate state user-data) candidate) 92 | :documentation "write what a renderer is.") 93 | ;; action: TODO: (cons action-name action-function) 94 | ;; TODO: add support for actions which do not kill the session 95 | (action (lambda (_source candidate) candidate) 96 | :documentation 97 | "Default action on the candidate when sallet session is finished. 98 | 99 | An action is a function of two arguments, the current source and 100 | the picked candidate.") 101 | ;; A function generating candidates, a list or vector of candidates. 102 | ;; Candidates can be either strings or any lists with first element 103 | ;; being used for matching (usually a string, but can be anything as 104 | ;; long as the matcher and renderer and action know how to handle 105 | ;; it). 106 | (candidates nil) 107 | ;; function generating candidates, takes source and state, returns a 108 | ;; vector The difference with `candidates' is that `candidates' run 109 | ;; only when we first enter the session while `generator' takes 110 | ;; input interactively. 111 | (generator nil) 112 | ;; If t, this source is asynchronous and processing takes place in a 113 | ;; different emacs instance. The source must be written with this in mind. 114 | (async nil) 115 | ;; Header. Is either a string or a function. In case of a string, 116 | ;; it is rendered in sallet-source-header face with a counter 117 | ;; showing number of filtered/all candidates. If a function, it is 118 | ;; passed current source as argument and its output is used verbatim 119 | ;; as the header text (newline is added *automatically*) 120 | (header "Select a candidate")) 121 | 122 | (sallet-defsource asyncio (default) 123 | "Default asyncio source." 124 | (process nil 125 | :documentation "Process generating candidates")) 126 | 127 | ;; TODO: replace all these setters and getters with the 128 | ;; accessor/setter property on the eieio object 129 | (defun sallet-source-get-matcher (source) 130 | (oref source matcher)) 131 | (defun sallet-source-get-sorter (source) 132 | (oref source sorter)) 133 | (defun sallet-source-get-init (source) 134 | (oref source init)) 135 | (defun sallet-source-get-renderer (source) 136 | (oref source renderer)) 137 | (defun sallet-source-get-candidates (source) 138 | (oref source candidates)) 139 | (defun sallet-source-get-generator (source) 140 | (oref source generator)) 141 | (defun sallet-source-get-header (source) 142 | (oref source header)) 143 | (defun sallet-source-get-action (source) 144 | (oref source action)) 145 | (defun sallet-source-get-processed-candidates (source) 146 | (oref source processed-candidates)) 147 | (defun sallet-source-is-async (source) 148 | (oref source async)) 149 | (defun sallet-source-get-process (source) 150 | (when (slot-exists-p source 'process) 151 | (oref source process))) 152 | (defun sallet-source-get-before-candidate-render-hook (source) 153 | (when (slot-exists-p source 'before-candidate-render-hook) 154 | (oref source before-candidate-render-hook))) 155 | (defun sallet-source-get-before-render-hook (source) 156 | (when (slot-exists-p source 'before-render-hook) 157 | (oref source before-render-hook))) 158 | 159 | (defun sallet-source-set-candidates (source candidates) 160 | (oset source candidates candidates)) 161 | (defun sallet-source-set-generator (source generator) 162 | (oset source generator generator)) 163 | (defun sallet-source-set-header (source header) 164 | (oset source header header)) 165 | (defun sallet-source-set-processed-candidates (source processed-candidates) 166 | (oset source processed-candidates processed-candidates)) 167 | (defun sallet-source-set-process (source process) 168 | (oset source process process)) 169 | 170 | (defun sallet-source-get-candidate (source n) 171 | (elt (sallet-source-get-candidates source) n)) 172 | 173 | (defun sallet-init-source (source) 174 | "Initiate the SOURCE." 175 | (-when-let (instance (funcall source (symbol-name source))) 176 | (funcall (sallet-source-get-init instance) instance) 177 | (let ((candidates (sallet-source-get-candidates instance))) 178 | (cond 179 | ((functionp candidates) 180 | (setq candidates (funcall candidates))) 181 | ((or (listp candidates) 182 | (vectorp candidates) 183 | (-when-let (sv (ignore-errors (symbol-value candidates))) 184 | (when (or (listp sv) (vectorp sv)) 185 | (setq candidates sv))))) 186 | ((functionp (sallet-source-get-generator instance)) 187 | (setq candidates nil)) 188 | (t (error "Invalid source: no way to generate candidates"))) 189 | (when (and candidates 190 | (not (vectorp candidates))) 191 | (setq candidates (vconcat candidates))) 192 | (sallet-source-set-candidates instance candidates) 193 | ;; no filtering at start 194 | (sallet-source-set-processed-candidates instance (sallet-make-candidate-indices candidates))) 195 | (let ((generator (sallet-source-get-generator instance))) 196 | (unless (functionp generator) 197 | (sallet-source-set-generator instance (eval generator t)))) 198 | instance)) 199 | 200 | (provide 'sallet-source) 201 | ;;; sallet-source.el ends here 202 | -------------------------------------------------------------------------------- /sallet-state.el: -------------------------------------------------------------------------------- 1 | ;;; sallet-state.el --- Sallet state -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 10th October 2015 9 | ;; Package-requires: ((dash "2.10.0")) 10 | ;; Keywords: convenience 11 | 12 | ;; This program is free software; you can redistribute it and/or 13 | ;; modify it under the terms of the GNU General Public License 14 | ;; as published by the Free Software Foundation; either version 3 15 | ;; of the License, or (at your option) any later version. 16 | 17 | ;; This program is distributed in the hope that it will be useful, 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | ;; GNU General Public License for more details. 21 | 22 | ;; You should have received a copy of the GNU General Public License 23 | ;; along with this program. If not, see . 24 | 25 | ;;; Commentary: 26 | 27 | ;; Sallet state keeps track of all the necessary information for a 28 | ;; sallet session. In theory we could run multiple sessions at the 29 | ;; same time, each contained within its state object. 30 | 31 | ;;; Code: 32 | 33 | (require 'dash) 34 | (require 'cl-lib) 35 | 36 | (require 'sallet-core) 37 | (require 'sallet-source) 38 | 39 | ;; TODO: make this into eieio object? 40 | ;; TODO: add documentation for the futures thing (and write/figure 41 | ;; out how the async works) 42 | (defvar sallet-state nil 43 | "Current state. 44 | 45 | SOURCES is a list of initialized sources. 46 | 47 | CURRENT-BUFFER is the buffer from which sallet was executed. 48 | 49 | PROMPT is the current prompt. 50 | 51 | SELECTED-CANDIDATE is the currently selected candidate. 52 | 53 | FUTURES is a plist mapping source id to the `async' future that 54 | computes it.") 55 | 56 | (defun sallet-state-get-sources (state) 57 | (cdr (assoc 'sources state))) 58 | (defun sallet-state-get-current-buffer (state) 59 | (cdr (assoc 'current-buffer state))) 60 | (defun sallet-state-get-prompt (state) 61 | (cdr (assoc 'prompt state))) 62 | (defun sallet-state-get-selected-candidate (state) 63 | (cdr (assoc 'selected-candidate state))) 64 | (defun sallet-state-get-candidate-buffer (state) 65 | (cdr (assoc 'candidate-buffer state))) 66 | (defun sallet-state-get-futures (state) 67 | (cdr (assoc 'futures state))) 68 | 69 | (defun sallet-state-set-sources (state sources) 70 | (setf (cdr (assoc 'sources state)) sources)) 71 | (defun sallet-state-set-prompt (state prompt) 72 | (setf (cdr (assoc 'prompt state)) prompt)) 73 | (defun sallet-state-set-selected-candidate (state selected-candidate) 74 | (setf (cdr (assoc 'selected-candidate state)) selected-candidate)) 75 | (defun sallet-state-set-futures (state futures) 76 | (setf (cdr (assoc 'futures state)) futures)) 77 | 78 | (defun sallet-state-incf-selected-candidate (state) 79 | (cl-incf (cdr (assoc 'selected-candidate state)))) 80 | (defun sallet-state-decf-selected-candidate (state) 81 | (cl-decf (cdr (assoc 'selected-candidate state)))) 82 | 83 | (defun sallet-state-get-number-of-all-candidates (state) 84 | "Return the number of all candidates in this STATE." 85 | (-sum (--map (length (sallet-source-get-processed-candidates it)) 86 | (sallet-state-get-sources state)))) 87 | 88 | (defun sallet--goto-candidate (state) 89 | "Move point to current candidate in STATE." 90 | (-when-let (pos (text-property-any 91 | (point-min) (point-max) 92 | 'sallet-candidate-index 93 | (sallet-state-get-selected-candidate state))) 94 | (goto-char pos))) 95 | 96 | (defun sallet-state--get-source-bounds (source state) 97 | "Find bounds of this SOURCE in current STATE." 98 | (with-current-buffer (sallet-state-get-candidate-buffer state) 99 | (save-excursion 100 | (-when-let (beg (text-property-any 101 | (point-min) (point-max) 102 | 'sallet-source (eieio-object-class source))) 103 | (let ((end (next-single-property-change (1+ beg) 'sallet-source))) 104 | (cons beg (or end (point-max)))))))) 105 | 106 | (defun sallet-state-get-selected-source (state) 107 | "Return the currently selected source and candidate. 108 | 109 | STATE is sallet state." 110 | (-when-let (sources 111 | (--filter (< 0 (length (sallet-source-get-processed-candidates it))) 112 | (sallet-state-get-sources state))) 113 | (let* ((offset (sallet-state-get-selected-candidate state)) 114 | (re (car sources)) 115 | (total 0) 116 | (total-old total)) 117 | (--each-while sources (<= total offset) 118 | (setq total-old total) 119 | (setq total (+ total (length (sallet-source-get-processed-candidates it)))) 120 | (setq re it)) 121 | (cons re (sallet-source-get-candidate 122 | re 123 | (sallet-car-maybe 124 | (let ((proc (sallet-source-get-processed-candidates re))) 125 | (nth (- offset total-old) proc)))))))) 126 | 127 | (defun sallet-init-state (sources candidate-buffer) 128 | "Initialize state with SOURCES in CANDIDATE-BUFFER." 129 | (let ((state (list (cons 'sources (-keep 'sallet-init-source sources)) 130 | (cons 'current-buffer (current-buffer)) 131 | (cons 'prompt "") 132 | (cons 'selected-candidate 0) 133 | (cons 'futures nil) 134 | (cons 'candidate-buffer candidate-buffer)))) 135 | (setq sallet-state state) 136 | state)) 137 | 138 | (provide 'sallet-state) 139 | ;;; sallet-state.el ends here 140 | -------------------------------------------------------------------------------- /sallet.el: -------------------------------------------------------------------------------- 1 | ;;; sallet.el --- Select candidates in a buffer -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2014-2015 Matúš Goljer 4 | 5 | ;; Author: Matúš Goljer 6 | ;; Maintainer: Matúš Goljer 7 | ;; Version: 0.0.1 8 | ;; Created: 31st December 2014 9 | ;; Keywords: convenience 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (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 | ;;; Code: 27 | (require 'dash) 28 | (require 's) 29 | (require 'async) 30 | (require 'flx) 31 | (require 'ov) 32 | (require 'f) 33 | 34 | (require 'eieio) 35 | (require 'ibuffer) 36 | (require 'imenu) 37 | 38 | (require 'sallet-core) 39 | (require 'sallet-source) 40 | (require 'sallet-state) 41 | (require 'sallet-filters) 42 | (require 'sallet-faces) 43 | 44 | (require 'sallet-buffer) 45 | (require 'sallet-recentf) 46 | (require 'sallet-imenu) 47 | (require 'sallet-occur) 48 | (require 'sallet-autobookmarks) 49 | (require 'sallet-bookmarks) 50 | (require 'sallet-projectile) 51 | (require 'sallet-registers) 52 | (require 'sallet-ag) 53 | (require 'sallet-man) 54 | 55 | (defgroup sallet () 56 | "Select candidates in a buffer." 57 | :group 'convenience 58 | :prefix "sallet-") 59 | 60 | (sallet-defsource default-directory-files () 61 | "List files in `default-directory.'" 62 | (candidates (-map 'f-filename (f-files default-directory))) 63 | (header "Default directory") 64 | (action (lambda (_ c) 65 | (find-file (concat default-directory "/" c))))) 66 | 67 | ;; TODO: add a user/source option to disable this 68 | (defun sallet--smart-case (pattern &optional switch) 69 | "Decide if we should turn on smart-case matching for PATTERN. 70 | 71 | If PATTERN contains upper-case letters, respect case, otherwise 72 | ignore case. 73 | 74 | SWITCH is the command switch which we should use to toggle this 75 | behaviour, defaults to \"--ignore-case\". 76 | 77 | Returns a list with car being the SWITCH." 78 | (let ((case-fold-search nil)) 79 | (unless (string-match-p "[A-Z]" pattern) 80 | (list (or switch "--ignore-case"))))) 81 | 82 | ;; TODO: move all the asyncio to a separate file 83 | (defun sallet-process-args (args) 84 | "Construct a list of arguments to pass to `start-process'. 85 | 86 | ARGS is a list of strings or lists. If a string, it is copied to 87 | the output list verbatim. If a list, its elements are copied to 88 | the output one by one, thus flattening the input by one level. 89 | This also means that nil inputs are ignored." 90 | (let (re) 91 | (-each args 92 | (lambda (arg) 93 | (if (listp arg) 94 | (when arg (--each arg (push it re))) 95 | (push arg re)))) 96 | (nreverse re))) 97 | 98 | (defun sallet-start-process (program &rest args) 99 | "Run PROGRAM with ARGS. 100 | 101 | ARGS are preprocessed using `sallet-process-args'." 102 | (declare (indent 1)) 103 | (apply 'start-process 104 | program nil program 105 | (sallet-process-args args))) 106 | 107 | (defun sallet-make-generator-linewise-asyncio (process-creator processor) 108 | "Make a linewise generator. 109 | 110 | PROCESS-CREATOR is a function which when called returns a process 111 | which produces the candidates. It takes one argument, the 112 | current prompt. 113 | 114 | PROCESSOR is a function taking one line of output and producing a 115 | candidate. 116 | 117 | Return a generator." 118 | (lambda (source state) 119 | (let ((prompt (sallet-state-get-prompt state))) 120 | (-when-let (proc (funcall process-creator prompt)) 121 | (sallet--kill-source-process source) 122 | (set-process-filter 123 | proc 124 | (sallet-process-filter-linewise-candidate-decorator 125 | processor source state)) 126 | (set-process-sentinel 127 | proc 128 | (lambda (_process process-state) 129 | (when (equal process-state "finished\n") 130 | (sallet-update-candidates state source) 131 | ;; TODO: do we want to render here? 132 | (sallet-render-state state t)))) 133 | (sit-for 0.01) 134 | proc)))) 135 | 136 | ;; TODO: rename to process-creator prefix 137 | (defun sallet-process-run-in-directory (process-creator directory) 138 | "Run PROCESS-CREATOR in DIRECTORY." 139 | (lambda (prompt) 140 | (when directory 141 | (with-temp-buffer 142 | (cd directory) 143 | (funcall process-creator prompt))))) 144 | 145 | (defun sallet-process-creator-first-token-only (process-creator) 146 | "Decorate PROCESS-CREATOR to only receive first input token. 147 | 148 | PROCESS-CREATOR is responsible for creating a process which will 149 | generate candidates for current session and is called each time 150 | the prompt changes. 151 | 152 | This decorator interprets the prompt and only passes the first 153 | whitespace-separated token to the decorated PROCESS-CREATOR. 154 | Further, if this token hasn't changed the process is not 155 | restarted." 156 | (let ((old "")) 157 | (lambda (prompt) 158 | (let ((input (split-string prompt " "))) 159 | (when (not (equal (car input) old)) 160 | (setq old (car input)) 161 | (funcall process-creator (car input))))))) 162 | 163 | (defun sallet-process-creator-min-prompt-length (process-creator &optional limit) 164 | "Decorate PROCESS-CREATOR to only run if prompt is longer than LIMIT. 165 | 166 | Default limit is 3 characters." 167 | (lambda (prompt) 168 | (when (>= (length prompt) (or limit 3)) 169 | (funcall process-creator prompt)))) 170 | 171 | ;; TODO: add arguments such as path and other "session" data we need 172 | ;; to pass to grep 173 | (defun sallet-grep-make-process-creator (file-name) 174 | "Return a process creator for grep sallet. 175 | 176 | FILE-NAME is the file we are grepping." 177 | (lambda (prompt) 178 | (sallet-start-process "grep" 179 | "-n" (sallet--smart-case prompt) prompt file-name))) 180 | 181 | (sallet-defsource grep (asyncio) 182 | "Grep." 183 | (generator 184 | (sallet-make-generator-linewise-asyncio 185 | (sallet-process-creator-min-prompt-length 186 | (sallet-process-creator-first-token-only 187 | (sallet-grep-make-process-creator (buffer-file-name)))) 188 | 'identity)) 189 | (matcher (sallet-make-matcher 190 | (sallet-ignore-first-token-filter 191 | (sallet-make-tokenized-filter 192 | 'sallet-filter-substring)))) 193 | (renderer (lambda (c _ _) c)) 194 | (action (lambda (_source c) 195 | (goto-char (point-min)) 196 | (forward-line (1- (string-to-number (car (split-string c ":")))))))) 197 | 198 | (defun sallet-grep () 199 | "Run grep sallet." 200 | (interactive) 201 | (sallet (list sallet-source-grep))) 202 | 203 | (defun sallet-gtags-files-make-process-creator (source) 204 | "Return a process creator for gtags-files sallet. 205 | 206 | SOURCE is the invoked sallet source." 207 | (lambda (prompt) 208 | (let ((args (-concat 209 | (list "-P") 210 | (sallet--smart-case prompt) 211 | ;; TODO: for this kind of flex matching we should 212 | ;; replace . with [^/] so that we search only in the 213 | ;; base name and not the directory tree. Additionally, 214 | ;; / does flex matching on the path and non-prefixed 215 | ;; second and further strings substring-match the entire 216 | ;; path (if the first token starts with /, we use . in 217 | ;; the pattern to get full list over the entire project) 218 | ;; (list (mapconcat 219 | ;; (lambda (x) (char-to-string x)) 220 | ;; (string-to-list prompt) 221 | ;; ".*")) 222 | (list (concat ".*" prompt ".*"))))) 223 | (sallet-source-set-header source (concat "global " (s-join " " args))) 224 | (apply 'start-process "global" nil "global" args)))) 225 | 226 | ;; TODO: after some timeout, start generating candidates automatically 227 | (sallet-defsource gtags-files (asyncio) 228 | "Grep." 229 | (generator 230 | (lambda (source state) 231 | (funcall 232 | ;; TODO: add some threading interface to compose these? It is 233 | ;; impossible to understand which argument comes to which 234 | ;; decorator 235 | (sallet-make-generator-linewise-asyncio 236 | (sallet-process-creator-min-prompt-length 237 | (sallet-process-creator-first-token-only 238 | (sallet-process-run-in-directory 239 | (sallet-gtags-files-make-process-creator source) 240 | (locate-dominating-file default-directory "GTAGS")))) 241 | 'identity) 242 | source state))) 243 | (project-root (locate-dominating-file default-directory "GTAGS")) 244 | (matcher sallet-matcher-default) 245 | ;; (matcher sallet-matcher-flx-then-substring) 246 | ;; TODO: add some sorter which does intelligent scoring for 247 | ;; substring matches 248 | ;; (sorter sallet-sorter-flx) 249 | (renderer (lambda (candidate _ user-data) 250 | (sallet-compose-fontifiers 251 | candidate user-data 252 | '(sallet-fontify-regexp-matches . :regexp-matches) 253 | '(sallet-fontify-flx-matches . :flx-matches)))) 254 | (action (lambda (source c) 255 | (find-file (concat (oref source project-root) "/" c)))) 256 | (header (lambda (source) 257 | (sallet--wrap-header-string 258 | (format "File in project %s" (oref source project-root)) 259 | source)))) 260 | 261 | ;;;###autoload 262 | (defun sallet-gtags-files () 263 | "Run gtags files sallet." 264 | (interactive) 265 | (sallet (list sallet-source-gtags-files))) 266 | 267 | (defun sallet-tags-make-process-creator () 268 | "Return a process creator for gtags tags sallet." 269 | (let ((old "")) 270 | (lambda (prompt) 271 | ;; TODO: Extract this "run on change of first token only" logic, 272 | ;; see `sallet-run-program-on-first'. 273 | (let ((input (split-string prompt " "))) 274 | (when (or (not old) 275 | (not (equal (car input) old))) 276 | (setq old (car input)) 277 | ;; TODO: write something to "start global in the root" or 278 | ;; figure out a way to print paths from root, not relative. 279 | (with-temp-buffer 280 | ;; TODO: this should come from outside? 281 | (cd (locate-dominating-file default-directory "GTAGS")) 282 | (apply 283 | 'start-process 284 | "global" nil "global" "--result" "grep" "-T" 285 | (-concat 286 | (sallet--smart-case (car input)) 287 | ;; TODO: extract this "match substring anywhere in the 288 | ;; string" logic 289 | (list (concat ".*" (car input) ".*"))) 290 | ;; TODO: extract this "fuzzy regexp" generator logic 291 | ;; (mapconcat 292 | ;; (lambda (x) (char-to-string x)) 293 | ;; (string-to-list (car input)) 294 | ;; ".*") 295 | ))))))) 296 | 297 | ;; TODO: add a stack so we can pop back from where we came 298 | (defun sallet-gtags-tags-action (source candidate) 299 | "Display tag CANDIDATE in its buffer." 300 | (-when-let (root (locate-dominating-file default-directory "GTAGS")) 301 | (save-match-data 302 | (let (file line) 303 | (string-match "^\\(.*?\\):\\(.*?\\):" candidate) 304 | ;; sigh... 305 | (setq file (match-string 1 candidate)) 306 | (setq line (match-string 2 candidate)) 307 | (find-file (concat root "/" file)) 308 | (goto-char (point-min)) 309 | (forward-line (1- (string-to-number line))) 310 | (recenter-top-bottom))))) 311 | 312 | (sallet-defsource gtags-tags (asyncio) 313 | "Run global(1) to generate tag candidates." 314 | (generator 315 | ;; TODO: We should be generating better candidates, not just lines 316 | ;; (identity) 317 | (sallet-make-generator-linewise-asyncio 318 | (sallet-tags-make-process-creator) 319 | 'identity)) 320 | (matcher 321 | ;; TODO: match only on content, add / matcher for path 322 | sallet-matcher-default) 323 | ;; TODO: better sorter 324 | ;; (sorter sallet-sorter-flx) 325 | (renderer 326 | ;; TODO: extract this renderer into a function, it is pretty common 327 | (lambda (candidate _ user-data) 328 | (sallet-compose-fontifiers 329 | candidate user-data 330 | '(sallet-fontify-regexp-matches . :regexp-matches) 331 | '(sallet-fontify-flx-matches . :flx-matches)))) 332 | (action sallet-gtags-tags-action)) 333 | 334 | ;;;###autoload 335 | (defun sallet-gtags-tags () 336 | "Run gtags tags sallet." 337 | (interactive) 338 | (sallet (list sallet-source-gtags-tags))) 339 | 340 | ;; TODO: add projectile support 341 | (defun sallet--set-search-root (source) 342 | "Set search root for SOURCE. 343 | 344 | The user is asked interactively for search root. 345 | 346 | If the `default-directory' is inside a gtags project, the project 347 | root is supplied as the default choice. 348 | 349 | Otherwise the user is asked to pick the search root starting at 350 | the `default-directory'." 351 | (unless (slot-exists-p source 'search-root) 352 | (error "Slot `search-root' does not exist")) 353 | (oset source search-root 354 | (read-directory-name 355 | "Project root: " 356 | (locate-dominating-file default-directory "GTAGS")))) 357 | 358 | ;; TODO: make this wrapper automatic 359 | (defun sallet-man () 360 | "Run man sallet." 361 | (interactive) 362 | (sallet (list sallet-source-man))) 363 | 364 | (defun sallet-ag () 365 | "Run ag sallet." 366 | (interactive) 367 | (sallet (list sallet-source-ag))) 368 | 369 | (defun sallet-ag-files () 370 | "Run ag sallet." 371 | (interactive) 372 | (sallet (list sallet-source-ag-files))) 373 | 374 | ;; TODO: restart the process only if args change 375 | (defun sallet-locate-make-process-creator (source &optional buffer) 376 | "Return a process creator for locate sallet. 377 | 378 | SOURCE is the invoked sallet source." 379 | (lambda (prompt) 380 | (let* ((tokens (split-string prompt)) 381 | (whole? (eq (aref prompt 0) ?/)) 382 | (all? (and whole? (>= (length tokens) 1))) 383 | (args (-concat 384 | ;; TODO: write something that dispatches on pattern like 385 | ;; we have for filters 386 | (list (if whole? "--wholename" "--basename")) 387 | (when all? (list "--all")) 388 | (sallet--smart-case prompt) 389 | (cons 390 | (if (eq (aref (car tokens) 0) ?/) 391 | (substring (car tokens) 1) 392 | (car tokens)) 393 | (when all? (cdr tokens)))))) 394 | (sallet-source-set-header source (concat "locate " (s-join " " args))) 395 | (apply 'start-process "locate" buffer "locate" args)))) 396 | 397 | (defun sallet-locate-filter-substring (candidates indices pattern) 398 | "Match CANDIDATES at INDICES against PATTERN. 399 | 400 | CANDIDATES is a vector of candidates. 401 | 402 | INDICES is a list of processed candidates. 403 | 404 | Uses substring matching. 405 | 406 | First, try to match the basename, then match the entire path. 407 | Files with match in the basename are tagged for priority sorting 408 | by the sorter. Also, regular files are tagged to be sorted over 409 | directories." 410 | (let ((quoted-pattern (regexp-quote pattern))) 411 | (-keep 412 | (lambda (index) 413 | (let* ((candidate (sallet-candidate-aref candidates index)) 414 | (base (f-base candidate)) 415 | (directory (f-dirname candidate)) 416 | (offset (length directory)) 417 | (offset (if (eq (aref directory (1- offset)) ?/) 418 | offset 419 | (1+ offset)))) 420 | (cond 421 | ((string-match quoted-pattern base) 422 | (sallet-update-index 423 | index 424 | (list :regexp-matches 425 | (cons 426 | (+ offset (match-beginning 0)) 427 | (+ offset (match-end 0))) 428 | 'cons) 429 | (list :is-base t) 430 | (list :is-file (f-file? candidate)))) 431 | ((string-match quoted-pattern directory) 432 | (sallet-update-index 433 | index 434 | (list :regexp-matches 435 | (cons (match-beginning 0) (match-end 0)) 436 | 'cons) 437 | (list :is-file (f-file? candidate))))))) 438 | indices))) 439 | 440 | (defun sallet-locate-action (_source c) 441 | (if (sallet--find-file-in-emacs-p c) 442 | (find-file c) 443 | (call-process "xdg-open" nil 0 nil c))) 444 | 445 | (defun sallet-locate-renderer (candidate _ user-data) 446 | (sallet-fontify-regexp-matches 447 | (plist-get user-data :regexp-matches) 448 | candidate)) 449 | 450 | (sallet-defsource locate (asyncio) 451 | "Run locate(1). 452 | 453 | Text files or directories are opened inside emacs while the rest 454 | is opened through xdg-open(1)." 455 | (generator 456 | (lambda (source state) 457 | (funcall 458 | (sallet-make-generator-linewise-asyncio 459 | (sallet-process-creator-min-prompt-length 460 | (sallet-locate-make-process-creator source)) 461 | 'identity) 462 | source state))) 463 | (matcher (lambda (candidates state) 464 | (let* ((prompt (sallet-state-get-prompt state)) 465 | (indices (sallet-make-candidate-indices candidates))) 466 | (sallet-compose-filters-by-pattern 467 | '(("\\`/\\(.*\\)" 1 sallet-locate-filter-substring) 468 | ("\\`\\.\\(.*\\)" 1 sallet-filter-file-extension) 469 | (t sallet-locate-filter-substring)) 470 | candidates 471 | indices 472 | prompt)))) 473 | (sorter (lambda (c _) 474 | (-sort (-lambda (a b) 475 | (if (and (consp a) (consp b)) 476 | (-let* (((_ &keys :is-base a :is-file fa) a) 477 | ((_ &keys :is-base b :is-file fb) b)) 478 | (cond 479 | ((and a b) 480 | (and fa (not fb))) 481 | (t (and a (not b))))) 482 | t)) 483 | c))) 484 | (renderer sallet-locate-renderer) 485 | (header "locate") 486 | (action sallet-locate-action)) 487 | 488 | (defun sallet-locate () 489 | "Run locate sallet." 490 | (interactive) 491 | (sallet (list sallet-source-locate))) 492 | 493 | (defun sallet--wrap-header-string (header-string source) 494 | "Wrap HEADER-STRING for SOURCE with meta information." 495 | (format 496 | " • %s [%d/%d]" 497 | header-string 498 | (length (sallet-source-get-processed-candidates source)) 499 | (sallet-vector-logical-length (sallet-source-get-candidates source)))) 500 | 501 | (defun sallet--propertize-header (header-string) 502 | "Propertize HEADER-STRING with default sallet header face." 503 | (propertize header-string 'face 'sallet-source-header)) 504 | 505 | (defun sallet-render-header (source) 506 | "Render header for sallet SOURCE." 507 | (let ((processed-candidates (sallet-source-get-processed-candidates source))) 508 | (when (and processed-candidates 509 | (> (length processed-candidates) 0)) 510 | (let* ((header (sallet-source-get-header source)) 511 | (header-string 512 | (if (functionp header) 513 | (let ((header-string (funcall header source))) 514 | (if (text-property-not-all 515 | 0 (length header-string) 'face nil header-string) 516 | header-string 517 | (sallet--propertize-header 518 | (concat header-string "\n")))) 519 | (sallet--propertize-header 520 | (concat (sallet--wrap-header-string header source) "\n"))))) 521 | (put-text-property 0 1 'sallet-source (eieio-object-class source) header-string) 522 | header-string)))) 523 | 524 | ;; TODO propertize the interesting stuff, define faces 525 | (defun sallet-render-source (source state offset) 526 | "Render SOURCE in STATE. 527 | 528 | OFFSET is the number of already rendered candidates before 529 | this source. 530 | 531 | Return number of rendered candidates." 532 | (with-current-buffer (sallet-state-get-candidate-buffer state) 533 | (let* ((processed-candidates (sallet-source-get-processed-candidates source)) 534 | (renderer (sallet-source-get-renderer source)) 535 | (before-render-hook 536 | (sallet-source-get-before-render-hook source)) 537 | (before-candidate-render-hook 538 | (sallet-source-get-before-candidate-render-hook source)) 539 | (i 0)) 540 | (when (functionp before-render-hook) 541 | (funcall before-render-hook source state)) 542 | (--when-let (sallet-render-header source) (insert it)) 543 | (-each processed-candidates 544 | (lambda (n) 545 | ;; `n' can be a number or a list returned from the 546 | ;; matcher---the `car' of which is then the index, the rest 547 | ;; is arbitrary meta data ignored at this stage (it is 548 | ;; useful when at the sorter stage) 549 | (let* ((candidate (sallet-source-get-candidate source (sallet-car-maybe n)))) 550 | (when (functionp before-candidate-render-hook) 551 | (funcall before-candidate-render-hook candidate state n)) 552 | (insert (propertize " " 'sallet-candidate-index (+ offset i)) 553 | ;; TODO: cache the already rendered lines also 554 | ;; between sallet calls, there's quite a lot of 555 | ;; chance it will come again, like with buffers or 556 | ;; so 557 | (or (funcall renderer candidate state (cdr-safe n)) 558 | (propertize "ERROR WHILE COMPUTING CANDIDATE" 'face font-lock-warning-face)) 559 | "\n")) 560 | (setq i (1+ i)))) 561 | i))) 562 | 563 | (defun sallet-render-state (state render-sources) 564 | "Render state. 565 | 566 | STATE is the current `sallet-state'. 567 | 568 | RENDER-SOURCES indicates whether we need to render sources (in 569 | case the prompt or candidates changed) or only update the 570 | scrolling/position of selected/marked candidate." 571 | (when render-sources 572 | (with-current-buffer (sallet-state-get-candidate-buffer state) 573 | (erase-buffer) 574 | (let ((offset 0)) 575 | (-each (sallet-state-get-sources state) 576 | (lambda (source) 577 | (setq offset (+ offset (sallet-render-source source state offset))))) 578 | (insert "\n\n")))) 579 | ;; Draw the >> pointer to the currently active candidate 580 | (with-current-buffer (sallet-state-get-candidate-buffer state) 581 | (-when-let (pos (text-property-any (point-min) (point-max) 'sallet-candidate-index (sallet-state-get-selected-candidate state))) 582 | (ov-clear 'sallet-selected-candidate-arrow) 583 | (goto-char pos) 584 | ;; TODO: add face, extend the overlay over the entire row? (then 585 | ;; we can highlight the rest with some overlay as well) 586 | (ov (point) (+ 2 (point)) 'display ">>" 'sallet-selected-candidate-arrow t) 587 | (set-window-point (get-buffer-window (sallet-state-get-candidate-buffer state)) pos)))) 588 | 589 | (defun sallet--kill-source-process (source) 590 | "Kill process associated with SOURCE, if any." 591 | (-when-let (old-proc (sallet-source-get-process source)) 592 | (set-process-filter old-proc nil) 593 | (set-process-sentinel old-proc nil) 594 | (ignore-errors (kill-process old-proc)))) 595 | 596 | (defun sallet-cleanup-candidate-window (state) 597 | "Cleanup the sallet STATE." 598 | (--each (sallet-state-get-sources state) 599 | (sallet--kill-source-process it)) 600 | (-when-let (buffer (get-buffer "*Sallet candidates*")) 601 | (kill-buffer buffer)) 602 | (--each (buffer-list) 603 | (when (string-match-p "\\` \\*Minibuf-[0-9]+\\*\\'"(buffer-name it)) 604 | (remove-hook 'post-command-hook 'sallet-minibuffer-post-command-hook t)))) 605 | 606 | (defvar sallet-minibuffer-post-command-hook nil 607 | "Closure used to update sallet window on minibuffer events. 608 | 609 | The closure is stored in function slot.") 610 | 611 | (defun sallet-minibuffer-setup (state) 612 | "Setup `post-command-hook' in minibuffer to update sallet STATE." 613 | ;; TODO: figure out where to do which updates... this currently 614 | ;; doesn't work The problem is that this lags the minibuffer input 615 | ;; reading every time it changes and some recomputation happens. We 616 | ;; want to be able to type in a word without sallet recomputing the 617 | ;; full candidate list after every letter. Helm solves this with 618 | ;; timers, which we will probably have to opt for too (aka poor 619 | ;; man's threads) 620 | (fset 'sallet-minibuffer-post-command-hook 621 | (lambda () (sallet-minibuffer-post-command state))) 622 | (add-hook 'post-command-hook 'sallet-minibuffer-post-command-hook nil t)) 623 | 624 | ;; TODO: figure out how to do the buffer passing fast 625 | (defun sallet-process-source-async (source state) 626 | "Process SOURCE in STATE asynchronously in separate Emacs." 627 | (let ((sallet-async-state (--remove 628 | (memq (car it) '(sources 629 | futures 630 | current-buffer 631 | candidate-buffer)) state)) 632 | (sallet-async-source source)) 633 | (async-start 634 | `(lambda () 635 | (push ,(file-name-directory (locate-library "dash")) load-path) 636 | (push ,(file-name-directory (locate-library "flx")) load-path) 637 | (push ,(file-name-directory (locate-library "shut-up")) load-path) 638 | (push ,(file-name-directory (locate-library "sallet")) load-path) 639 | (require 'shut-up) 640 | (require 'sallet) 641 | (with-temp-buffer 642 | (shut-up 643 | ;; TODO: this isn't always necessary, should be part of the 644 | ;; async source generator? 645 | (insert ,(with-current-buffer (sallet-state-get-current-buffer state) 646 | (buffer-substring-no-properties (point-min) (point-max)))) 647 | (setq sallet-async-state (read ,(format "%S" sallet-async-state))) 648 | (setq sallet-async-source (read ,(format "%S" sallet-async-source))) 649 | (sallet-process-source sallet-async-state sallet-async-source)) 650 | (list :candidates (sallet-source-get-candidates sallet-async-source) 651 | :processed-candidates (sallet-source-get-processed-candidates sallet-async-source)))) 652 | (lambda (result) 653 | (-when-let ((&plist :candidates candidates 654 | :processed-candidates processed-candidates) 655 | result) 656 | (sallet-source-set-candidates source candidates) 657 | (sallet-source-set-processed-candidates source processed-candidates) 658 | (sallet-render-state state t)))))) 659 | 660 | (defun sallet-update-candidates (state source) 661 | "Update candidates and processed-candidatess in STATE for SOURCE." 662 | (let* ((candidates (sallet-source-get-candidates source))) 663 | (-if-let (matcher (sallet-source-get-matcher source)) 664 | (let ((processed-candidates (funcall matcher candidates state))) 665 | (sallet-source-set-processed-candidates source processed-candidates)) 666 | (sallet-source-set-processed-candidates source (sallet-make-candidate-indices candidates)))) 667 | (let* ((processed-candidates (sallet-source-get-processed-candidates source))) 668 | (-when-let (sorter (sallet-source-get-sorter source)) 669 | (sallet-source-set-processed-candidates 670 | source 671 | (funcall sorter processed-candidates state))))) 672 | 673 | (defun sallet-process-source (state source) 674 | "Update sallet STATE by processing SOURCE." 675 | (-when-let (generator (sallet-source-get-generator source)) 676 | (let ((gen (funcall generator source state))) 677 | (cond 678 | ((processp gen) 679 | (sallet-source-set-process source gen)) 680 | (gen (sallet-source-set-candidates source gen))))) 681 | (sallet-update-candidates state source)) 682 | 683 | (defun sallet-process-sources (state) 684 | "Process all sallet sources in STATE. 685 | 686 | There are three principal types of sources: sync, async and 687 | asyncio." 688 | ;; TODO: add old-prompt to state 689 | ;; TODO: add old-processed-candidates to state 690 | (-each (sallet-state-get-sources state) 691 | (lambda (source) 692 | ;; Here async means async package (computing in background 693 | ;; emacs). Another meaning of async is async io (using output 694 | ;; of a process to construct candidates). The latter is not yet 695 | ;; supported, but we should get it working. 696 | ;; TODO: add support for taking process output and constructing 697 | ;; list of candidates out of that. That could be called 698 | ;; "process filtering source" ? 699 | (if (not (sallet-source-is-async source)) 700 | (sallet-process-source state source) 701 | (let ((futures (sallet-state-get-futures state)) 702 | (source-id (aref source 2))) 703 | (-when-let ((&plist source-id process) futures) 704 | (ignore-errors 705 | (let ((buffer (process-buffer process))) 706 | (kill-process process) 707 | (kill-buffer buffer)))) 708 | (let ((proc (sallet-process-source-async source state))) 709 | (sallet-state-set-futures state (plist-put futures source-id proc)))))))) 710 | 711 | (defun sallet-minibuffer-post-command (state) 712 | "Function called in `post-command-hook' when sallet STATE is active. 713 | 714 | This function is added to minibuffer's `post-command-hook' and 715 | updates the candidate buffer." 716 | (let ((old-prompt (sallet-state-get-prompt state)) 717 | (new-prompt (buffer-substring-no-properties 5 (point-max)))) 718 | (unless (equal old-prompt new-prompt) 719 | (sallet-state-set-selected-candidate state 0) 720 | (sallet-state-set-prompt state new-prompt) 721 | (sallet-process-sources state)) 722 | (sallet-render-state state (not (equal old-prompt new-prompt))))) 723 | 724 | ;; TODO: add user-facing documentation as docstring and developer 725 | ;; documentation in code. 726 | ;; TODO: add a way to preprocess the pattern before passing it to the 727 | ;; individual sources... this can help when we mix "incompatible" 728 | ;; sources together (where e.g. special prefixes mean different things 729 | ;; or are meaningless... so if we mix e.g. buffer and locate, we don't 730 | ;; want to pass leading / to locate but we want to pass it to buffer). 731 | ;; The filter is not done on the source level because the same prefix 732 | ;; (or lack of) can mean different thing to different sources. 733 | ;; TODO: add conditional evaluation of sources. For example, first 734 | ;; run global -P, if nothing is found run ag -g, if nothing is found 735 | ;; run find . -name '**', if nothing is found run locate... This 736 | ;; way we don't run all the redundant "broader" searches if some 737 | ;; narrower search succeeds. After some timeouts or a "recompute 738 | ;; signal" we can recompute all targets. 739 | ;; TODO: add some simple default implementation for "line candidates 740 | ;; from process" and "grep-like candidates from process" 741 | (defun sallet (sources) 742 | "Run sallet SOURCES." 743 | (let* ((buffer (get-buffer-create "*Sallet candidates*")) 744 | ;; make this lexically scoped 745 | (state (sallet-init-state sources buffer))) 746 | ;; TODO: add better modeline, show number of sources/candidates etc... 747 | ;; TODO: add sallet-candidates-mode as major-mode 748 | (with-current-buffer buffer 749 | (kill-all-local-variables) 750 | ;; FIXME: hotfix against sql-workbench 751 | (setq-local font-lock-keywords nil) 752 | (setq truncate-lines t) 753 | (buffer-disable-undo) 754 | (setq cursor-type nil)) 755 | ;; TODO: if we have actions which could use "current" buffer 756 | ;; during session (e.g. show context of this occur line), we 757 | ;; should show that buffer in a separate (existing?) window. The 758 | ;; operations to restore the original state should go into 759 | ;; `sallet-cleanup-candidate-window'. See also 760 | ;; `helm-always-two-windows'. 761 | (switch-to-buffer buffer) 762 | (sallet-render-state state t) 763 | (condition-case _var 764 | (minibuffer-with-setup-hook (lambda () (sallet-minibuffer-setup state)) 765 | ;; TODO: add support to pass maps 766 | ;; TODO: propertize prompt 767 | (read-from-minibuffer 768 | ">>> " nil 769 | (let ((map (make-sparse-keymap))) 770 | (set-keymap-parent map minibuffer-local-map) 771 | (define-key map (kbd "C-n") 'sallet-candidate-up) 772 | (define-key map (kbd "C-p") 'sallet-candidate-down) 773 | (define-key map (kbd "C-o") 'sallet-candidate-next-source) 774 | (define-key map (kbd "C-v") 'sallet-scroll-up) 775 | (define-key map (kbd "M-v") 'sallet-scroll-down) 776 | map)) 777 | (sallet-default-action)) 778 | ;; TODO: do we want `kill-buffer-and-window?' 779 | (quit (sallet-cleanup-candidate-window state)) 780 | (error (sallet-cleanup-candidate-window state))))) 781 | 782 | ;; TODO: figure out how to avoid the global state here: sallet-state 783 | (defun sallet-candidate-up () 784 | "Move up one candidate in the candidate buffer." 785 | (interactive) 786 | (when (< (sallet-state-get-selected-candidate sallet-state) 787 | (1- (sallet-state-get-number-of-all-candidates sallet-state))) 788 | (sallet-state-incf-selected-candidate sallet-state))) 789 | 790 | (defun sallet-candidate-down () 791 | "Move down one candidate in the candidate buffer." 792 | (interactive) 793 | (when (> (sallet-state-get-selected-candidate sallet-state) 0) 794 | (sallet-state-decf-selected-candidate sallet-state))) 795 | 796 | ;; TODO: abstract the work with text properties 797 | (defun sallet-candidate-next-source () 798 | "Set the current selected candidate to the first candidate of next source." 799 | (interactive) 800 | (with-current-buffer (sallet-state-get-candidate-buffer sallet-state) 801 | (save-excursion 802 | (when (sallet--goto-candidate sallet-state) 803 | (let ((next (next-single-property-change (point) 'sallet-source))) 804 | (if (not next) 805 | (sallet-state-set-selected-candidate sallet-state 0) 806 | (goto-char next) 807 | (forward-line) 808 | (--if-let (get-text-property (point) 'sallet-candidate-index) 809 | (sallet-state-set-selected-candidate sallet-state it) 810 | (sallet-state-set-selected-candidate sallet-state 0)))))))) 811 | 812 | (defun sallet--scroll-offset () 813 | "Get the offset for scrolling up/down." 814 | (/ (window-height 815 | (get-buffer-window 816 | (sallet-state-get-candidate-buffer sallet-state))) 2)) 817 | 818 | (defun sallet-scroll-up () 819 | "Scroll candidates upwards, revealing later candidates." 820 | (interactive) 821 | (let ((index (min (+ (sallet-state-get-selected-candidate sallet-state) 822 | (sallet--scroll-offset)) 823 | (1- (sallet-state-get-number-of-all-candidates sallet-state))))) 824 | (sallet-state-set-selected-candidate sallet-state index))) 825 | 826 | (defun sallet-scroll-down () 827 | "Scroll candidates downwards, revealing previous candidates." 828 | (interactive) 829 | (let ((index (max (- (sallet-state-get-selected-candidate sallet-state) 830 | (sallet--scroll-offset)) 831 | 0))) 832 | (sallet-state-set-selected-candidate sallet-state index))) 833 | 834 | (defun sallet-default-action () 835 | "Default sallet action." 836 | (sallet-cleanup-candidate-window sallet-state) 837 | (-when-let ((source . cand) (sallet-state-get-selected-source sallet-state)) 838 | (funcall (sallet-source-get-action source) source cand))) 839 | 840 | (defun sallet-buffer () 841 | "Display buffer-like candidates. 842 | 843 | Takes the list of used sources from `sallet-buffer-sources'." 844 | (interactive) 845 | (sallet sallet-buffer-sources)) 846 | 847 | (defun sallet-occur () 848 | "Show all lines in current buffer matching the fuzzy pattern. 849 | 850 | First, all lines matching the input pattern \"fuzzily\" are 851 | collected. They are then scored and ordered to bring the most 852 | interesting lines at the top. Therefore, the results are not in 853 | the same order as they appear in the buffer. If you want that, 854 | use `salet-occur-nonfuzzy'. 855 | 856 | The scoring algorithm is from the package `flx'. 857 | 858 | If you want to customize the matching algorithm, you can extend 859 | sallet source `sallet-source-occur-fuzzy'." 860 | (interactive) 861 | (sallet (list sallet-source-occur-fuzzy))) 862 | 863 | (defun sallet-occur-async () 864 | "Run async occur sallet." 865 | (interactive) 866 | (sallet (list sallet-source-occur-async))) 867 | 868 | (defun sallet-occur-nonfuzzy () 869 | "Show all lines in current buffer matching pattern. 870 | 871 | The lines are presented in the same order as they appear in the 872 | file. The lines are matched against each word in the input 873 | separately. 874 | 875 | See also `sallet-occur' for a fuzzy variant. If you want to 876 | customize the matching algorithm, you can extend sallet source 877 | `sallet-source-occur'." 878 | (interactive) 879 | (sallet (list sallet-source-occur))) 880 | 881 | ;;;###autoload 882 | (defun sallet-imenu () 883 | "Run imenu sallet." 884 | (interactive) 885 | (sallet (list sallet-source-imenu))) 886 | 887 | ;; TODO: write sallet for opening files 888 | 889 | (defun sallet-process-filter-line-buffering-decorator (filter) 890 | "Decorate a process FILTER with line-buffering logic. 891 | 892 | Return a new process filter based on FILTER. 893 | 894 | FILTER is a function which could be used with `set-process-filter'. 895 | 896 | This decorator buffers input until it can pass a complete line 897 | further to the supplied FILTER. It is useful as a buffer between 898 | a process producing data and an Emacs function operating on the 899 | data which expects to get complete lines as input." 900 | (let ((data "")) 901 | (lambda (process string) 902 | (let* ((line-data (split-string (concat data string) "\n"))) 903 | (while (cdr line-data) 904 | (funcall filter process (car line-data)) 905 | (!cdr line-data)) 906 | (setq data (car line-data)))))) 907 | 908 | (defun sallet-process-filter-linewise-candidate-decorator (processor source state) 909 | "Turn a PROCESSOR into a candidate generating process filter. 910 | 911 | PROCESSOR is a function which given a string (usually a complete 912 | line of output of a program) generates one candidate from it. 913 | 914 | SOURCE is an instance of sallet source. The generated candidates 915 | are placed in this source's candidates vector. 916 | 917 | STATE is a sallet state." 918 | (let ((n 0)) 919 | ;; TODO: abstract the vector logic here into some "dynamic array" 920 | ;; data structure. Write tests for it. 921 | (sallet-source-set-candidates source (make-vector 32 nil)) 922 | (sallet-process-filter-line-buffering-decorator 923 | (lambda (_process string) 924 | (let* ((buffer (sallet-source-get-candidates source)) 925 | (cand (funcall processor string)) 926 | (bl (length buffer))) 927 | (when (= n bl) 928 | (let ((new-buffer (make-vector (* 2 bl) nil)) 929 | (i 0)) 930 | (mapc (lambda (x) 931 | (aset new-buffer i x) 932 | (setq i (1+ i))) 933 | buffer) 934 | (setq buffer new-buffer) 935 | (sallet-source-set-candidates source new-buffer))) 936 | (when (= (mod n 256) 0) 937 | (sallet-update-candidates state source) 938 | ;; TODO: do we really want to render from here? Seems like 939 | ;; too tight coupling. Maybe fire some "redraw please" 940 | ;; event 941 | (sallet-render-state state t)) 942 | (aset buffer n cand) 943 | (setq n (1+ n)) 944 | (sallet-source-set-candidates source buffer)))))) 945 | 946 | ;;;###autoload 947 | (defun sallet-register-point () 948 | "Sallet for point registers." 949 | (interactive) 950 | (sallet (list sallet-source-register-point))) 951 | 952 | (defun sallet-bookmarks () 953 | "Sallet for bookmarks" 954 | (interactive) 955 | (sallet (list sallet-source-bookmarks))) 956 | 957 | (provide 'sallet) 958 | 959 | ;; Local Variables: 960 | ;; eval: (add-to-list 'imenu-generic-expression '("Sallet sources" "\\(^(sallet-defsource +\\)\\(\\_<.+?\\_>\\)" 2)) 961 | ;; End: 962 | 963 | ;;; sallet.el ends here 964 | -------------------------------------------------------------------------------- /tests/sallet-start-process-test.el: -------------------------------------------------------------------------------- 1 | (require 'sallet) 2 | 3 | (describe "Process helpers" 4 | (it "should construct argument lists" 5 | 6 | (expect 7 | (sallet-process-args (list "grep" "-n" (when t (list "-E" "foo")) "this.el")) 8 | :to-equal 9 | (list "grep" "-n" "-E" "foo" "this.el")) 10 | 11 | (expect 12 | (sallet-process-args (list "grep" "-n" (when nil (list "-E" "foo")) "this.el")) 13 | :to-equal 14 | (list "grep" "-n" "this.el")) 15 | (expect 16 | (sallet-process-args (list "grep" "-n" "this.el")) 17 | :to-equal 18 | (list "grep" "-n" "this.el")) 19 | 20 | (expect 21 | (sallet-process-args (list "grep" "-n" (when t "foo") "this.el")) 22 | :to-equal 23 | (list "grep" "-n" "foo" "this.el")))) 24 | --------------------------------------------------------------------------------