├── .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 [](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 |
--------------------------------------------------------------------------------