Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC/Draft: add selectrum-options in order to allow overwriting options #244

Closed
wants to merge 1 commit into from
Closed

Conversation

minad
Copy link
Contributor

@minad minad commented Dec 1, 2020

This simple patch would solve most configuration problems if one wants to use the completing-read API exclusively but still wants the option to tweak some of the selectrum options, which will only be effective if selectrum-mode is enabled. Degradation to other completion-systems will be graceful.

(setq selectrum-options '(:no-move-default-candidate t))
(completing-read "Test: " '("one" "two" "three") nil nil nil nil "two")

See #243 for the discussion

@raxod502 @clemera

(setq selectrum-options '(:no-move-default-candidate t))
(completing-read "Test: " '("one" "two" "three") nil nil nil nil "two")
@minad minad marked this pull request as draft December 1, 2020 07:04
minad added a commit to minad/consult that referenced this pull request Dec 1, 2020
@clemera
Copy link
Collaborator

clemera commented Dec 1, 2020

I think it would be nice if we can avoid using minibuffer-with-setup-hook completely. For example I might want to set selectrum-display-action to something different for a single call, too. Maybe we can use something like (varname value...) as the plist to allow adjusting any variable for the duration of a call without having to mess around using minibuffer-with-setup-hook.

@minad
Copy link
Contributor Author

minad commented Dec 1, 2020

@clemera Yes, I thought about that too. The POC here is the simplest thing which achieves what I want right now. In order to allow more tuning, I suggest adding more options to selectrum-read and allow overwriting specific variables in that way. This would automatically work with this proposal. Furthermore the goal of this proposal is to make it easy to do minor tweaking with graceful degradation and therefore it is good that it is restricted. If I a would want to do more intrusive modifications there is no point in having something like this. Then it would be better to use selectrum-read directly. This is a compromise.

I guess your proposal would also work somehow but I find it both too general (allows arbitrary variables (?) and is in not tied to the api offered by selectrum) and at the same time not general enough. If one wants the general thing, I would rather add some kind of one-shot initialization hook. But then I think it is better if things are restricted.

@clemera
Copy link
Collaborator

clemera commented Dec 1, 2020

Yes, I thought about that too. The POC here is the simplest thing which achieves what I want right now.

Okay, but we need to think carefully because once we introduce this it's hard to make changes without breaking user code.

In order to allow more tuning, I suggest adding more options to selectrum-read and allow overwriting specific variables in that way. This would automatically work with this proposal.

That also means we would need to add more and more keywords over time and we will get issues where people will ask to include this or that as a keyword so it can be used to configure some variable for a single selectrum session. If we do this I think we should add every variable as a keyword upfront but in this case why not use a different approach? Ideally selectrum-read shouldn't be used, we want people to use completing-read so I think it make sense to have everything configurable while using it. It also is a big plus to have the same symbols to configure options for single call as are used to configure selectrum in general and a nice side effect is you would get completion for selectrum-options.

I guess your proposal would also work somehow but I find it both too general (allows arbitrary variables (?) and is in not tied to the api offered by selectrum) and at the same time not general enough. If one wants the general thing, I would rather add some kind of one-shot initialization hook.

We can restrict it to only allow selectrum variables so this would be still tied to the API we offer. I would prefer this approach because of the reasons above, I think it will require less maintenance and you get completion...you really want completion don't you? ;)

@minad
Copy link
Contributor Author

minad commented Dec 1, 2020

Okay, but we need to think carefully because once we introduce this it's hard to make changes without breaking user code.

Agree. Therefore I am aiming for a more minimal thing. Alternatively, we could just decide to do nothing. There are arguments against doing this since it somehow works around the existing api. I can use advices as I am doing right now and this could be done similarly for every supported completion system. However since selectrum in particular encourages using completing-read, I would like to see something better. But it is a pragmatic compromise, that's it.

Ideally selectrum-read shouldn't be used, we want people to use completing-read so I think it make sense to have everything configurable while using it. It also is a big plus to have the same symbols to configure options for single call as are used to configure selectrum in general and a nice side effect is you would get completion for selectrum-options.

I am not sure. You want something different than I want I guess. I am perfectly happy with having things restricted to selectrum-read options, there would not be additional documentation effort, no additional testing effort hopefully. Since it is restricted, fewer things can break. However I understand that one could be tempted to reach for the more powerful thing. But then you can always use the API directly and you can always advice functions. For more complex modifications all the machinery is already there and then it also does not hurt as much if you have to do more work.

We can restrict it to only allow selectrum variables so this would be still tied to the API we offer. I would prefer this approach because of the reasons above, I think it will require less maintenance and you get completion...you really want completion don't you? ;)

Sure, this is possible, I thought about that too. What do you mean - you get completion? Like completion at point? I think this is a rather minor advantage, I cannot have all the nice things it seems ;)

Note that there is already some inconsistency between what can be overwritten by variables and what an be overwritten by properties. What I would like is that the API makes a distinction here - there are public variables which should be used for more global configuration, e.g., overwriting the highlight function etc.

And then there are the local options, which should be set per selectrum-read call. These are the once I am interested in here. For example, I would like to overwrite initial-input and no-move-default-candidate. I think given your proposal I could only overwrite the move-default variable ant that would not help me. According to this distinction, selectrum-should-sort-p should go away and should be replaced by :sort or :no-sort (Obviously if completing-read is used, it is preferable to use a candidate function to configure the sorting).

@minad
Copy link
Contributor Author

minad commented Dec 1, 2020

@clemera I am experimenting with something else now (minad/consult@0d2c229), which also works nicely, is more local since it does not need an extra variable. I am happy with that. Doing nothing here is an acceptable option.

(defun consult--selectrum-config (options)
  "Add OPTIONS to the next `selectrum-read' call."
  (when (and options (bound-and-true-p selectrum-mode))
    (letrec ((advice (lambda (orig prompt candidates &rest args)
                       (advice-remove #'selectrum-read advice)
                       (apply orig prompt candidates (append options args)))))
      (advice-add #'selectrum-read :around advice))))

So to conclude - to do the kind of overwriting you have in mind (or other more complicated modifications), minibuffer-with-setup-hook is the right thing (given you merged #242). For the overwriting of options I have in mind, something like my consult--selectrum-config is a good solution.

@minad
Copy link
Contributor Author

minad commented Dec 1, 2020

Should I close this?

@clemera
Copy link
Collaborator

clemera commented Dec 1, 2020

I am perfectly happy with having things restricted to selectrum-read options, there would not be additional documentation effort, no additional testing effort hopefully. Since it is restricted, fewer things can break.

This is also true when we allow to set variables. The only difference is we allow to set them for a single call but they are still the same options which are already documented/tested as the global ones.

What do you mean - you get completion? Like completion at point?

Yes, that was what I meant.

And then there are the local options, which should be set per selectrum-read call. These are the once I am interested in here. For example, I would like to overwrite initial-input and no-move-default-candidate. I think given your proposal I could only overwrite the move-default variable ant that would not help me. According to this distinction, selectrum-should-sort-p should go away and should be replaced by :sort or :no-sort (Obviously if completing-read is used, it is preferable to use a candidate function to configure the sorting).

I think the case of inital-input indicates we should allow passing initial-input from selectrum-completing-read. Having to use this to override options passed to completing-read does not sound good.

Should I close this?

Sure, if you are not interested going forward with this. But I still would like to hear @raxod502 opinion as I like the idea of introducing something like selectrum-options as it makes things easier for users. Many don't know about the peculiarities of let binding selectrum variables vs. the usage of minibuffer-setup-hook + local variable bindings so it would be nice to offer an easy way which we can advertise to be used for configuring the very next selectrum session around completing-read calls.

@minad
Copy link
Contributor Author

minad commented Dec 1, 2020

I think the case of inital-input indicates we should allow passing initial-input from selectrum-completing-read. Having to use this to override options passed to completing-read does not sound good.

Yes it is not, but I don't want to tackle all the issues at once. This should also go with an upstream proposal to un-deprecate the option as we discussed before ;)

Sure, if you are not interested going forward with this. But I still would like to hear @raxod502 opinion as I like the idea of introducing something like selectrum-options as it makes things easier for users. Many don't know about the peculiarities of let binding selectrum variables vs. the usage of minibuffer-setup-hook + local variable bindings so it would be nice to offer an easy way which we can advertise to be used for configuring the very next selectrum session around completing-read calls.

Fine with me, I would also hear @raxod502's opinion! But the status quo is also okay for me - depending on what is available, I will adapt consult. Instead of adding anything to selectrum, we could just document the two idioms somewhere.

okamsn pushed a commit to okamsn/consult that referenced this pull request Dec 6, 2020
minad referenced this pull request in minad/marginalia Dec 6, 2020
The file annotations where only being displayed for files in the
current directory. This was because the candidates in file completion
are not the full path, but rather just the last path component.
The function file-attributes wants either a full path or a path
relative to the current directory.

To fix this bug I added a general function to calculate the "full
candidate", i.e, what the minibuffer contents would be if a given
candidate is chosen. For most types of completion, the completion
candidates are already full candidates. The main exception is file
name completion, where the candidates are one path component of the
full candidate, which is a full path. (There are other exceptions,
such as environment variable name completion inside file name
completion.)
@minad
Copy link
Contributor Author

minad commented Dec 14, 2020

I retract this. I prefer if selectrum would use completing-read metadata for configuration. Furthermore @clemera is working on the variable based configuration system which is different from this proposal.

@raxod502
Copy link
Member

Hey y'all! Sorry for the wait. These proposals are very interesting and well discussed. I am excited to see improvements in the area of configuring Selectrum sessions, because I think the current way of doing it is rather poor. I think the most important guiding principle is to develop some way to configure Selectrum such that:

  1. you don't have to use selectrum-read; that is, you could apply the configuration even if you're not the one calling into Selectrum
  2. it doesn't mess up recursive minibuffer sessions; that is, you can apply a configuration to only the next Selectrum session rather than all of the recursive ones
  3. the user doesn't have to know about minibuffer-local variables or the minibuffer setup hook
  4. the user doesn't have to know how to make a custom completion table function just to configure Selectrum
  5. (nice to have) the user can do this customization on a per-command basis (whatever that means to them) without having to redefine those commands

Now, from what I see in the discussion in this thread and #243 (trying to catch up 🙂), another question to ask is how configuration for Selectrum should be encoded. There are a few separate questions here:

  • Should the information be applied in a user option, or should it be in the metadata of the collection according to the completing-read format?
  • Should the information be in the format of keyword arguments to selectrum-read, or an alist of values for Selectrum user options, or some other format?
  • Should we support arbitrary code execution in this configuration API, or just a static set of bindings for user options or other properties?

I also see some discussion about whether we could deprecate selectrum-read and instead ask everyone to go through completing-read.

Now, let me give my comments.

deprecating selectrum-read

I originally wrote the API of selectrum-read to be a (slight) improvement over that of completing-read. However, since then a great ecosystem has emerged of packages cooperatively sharing the completing-read API, and I think this is a great direction for the community. I think it would be a great idea to move in the direction of deprecating selectrum-read as a separate function, and instead provide all the added functionality of selectrum-read via this new configuration API that will be fully based on the foundation of completing-read.

I think in order to implement this, we'd re-route all of the Selectrum functions that currently call into selectrum-read to instead go through completing-read, using the new configuration interface. Then we could adjust selectrum-read to do the same, probably moving much of the internal logic of selectrum-read into selectrum-completing-read-function instead.

new configuration interface

Here's one way that we might implement the configuration interface. It is only a starting point and I know you two have spent much more time thinking about the tradeoffs than I have, so please let me know what considerations I've missed. I think the proof will be in trying to rewrite existing uses of selectrum-read and the minibuffer-with-setup-hook tricks to go through the new interface. That will likely reveal any shortcomings, as I think we have exercised quite a bit of creativity already in the range of use cases in existing code (both in Selectrum proper as well as in Consult).

We could start by introducing a canonical list of possible alist keys, all optional, corresponding to things that are configurable about Selectrum. The default values in the cases that keys are unset might be configurable by some user options, if we want. Then we could introduce a selectrum-session-config user option which could be let-bound to an alist with such keys, as well as a selectrum-default-config user option which could be globally set to affect all sessions. The idea would be that we could read selectrum-session-config (default value could be a special symbol default) in selectrum-completing-read-function; if it's unset, then use selectrum-default-config, and otherwise use the value provided but then recursively let-bind selectrum-session-config back to default before entering the minibuffer. So far this would address (1) through (4), I think.

completing-read metadata

One notable point with this idea is that configuration is not passed as completing-read metadata. I like the idea of binding configuration to the collection, rather than as a separate system, since that reinforces the idea of different packages cooperating over the completing-read interface. However, it does have the disadvantage that users who want to pass configuration to Selectrum would have to understand how to create a custom completion table function, or we would have to introduce a helper to do this, which seems like it might be more surprising to people than being able to use let-binding.

We can also have multiple entry points to this configuration system, once the core data model is established. For example, we could support in selectrum-completing-read-function a special completion table metadata property selectrum-config which would allow a Selectrum config alist to be passed as part of a collection. That way, we could also allow the completing-read metadata model, as well as the let-binding model, without too much added complexity. Perhaps you can fill me in more on the reasons why the completing-read metadata model might be desirable to encourage, though. I might not be understanding the goals.

arbitrary minibuffer setup

I think I've seen at least one or two people asking to be able to run arbitrary code on minibuffer setup with Selectrum. We can accommodate that if one of the configuration keys in the alist is an optional function to execute at this point, and I think it would be pretty straightforward to support. An advantage with doing things this way is that it might allow us to get (5) essentially for free without introducing any black magic into Selectrum itself. For example, someone could configure the setup function key in selectrum-default-config to a function that would check this-command and conditionally set some configuration. I think it's become clear that this kind of thing is something people want to do, and it's frustrating that I've always shut that down due to concerns about it being a giant hack. But if we make it really straightforward to do that hack in a user's personal configuration, then we can get the best of both worlds: we're making it explicit that there's no objectively right way to determine "what command is being run", but you can easily decide for yourself what way will work for your use case.

I think we would want to make-local-variable on selectrum-session-config when setting up the minibuffer, because this way the setup function, if configured, could tweak it using setq.

some other thoughts

Why not make the keys in the alist the same names as the user options for Selectrum? Well, I had that written originally, but then I realized that people were probably going to think that they should let-bind the user options, and that won't work the way they want. An easy way to prevent people from using the user options the wrong way is to make them not user options anymore. It's a little less friendly, but we'd get rid of all these recursive minibuffer problems once and for all.

closing

So, yeah! This ended up being longer than I expected. But this is what came to mind (and went through a few revisions and changes of mind before posting) as I read the discussion. Let me know what you think! I suspect there will be many details to work out, and I probably missed some important points that may mean we'll need to go in a different direction from what I suggested.

@minad
Copy link
Contributor Author

minad commented Dec 15, 2020

  1. deprecating selectrum-read - I agree with this. It would be great if we can move into that direction, since we can unify the ecosystem behind that API. If Selectrum makes that move, it would be a good statement. The configuration issue is one thing - the other thing is that we have to get a working story for dynamic completion tables, which does not degrade the snappy selectrum completion behavior.

  2. new configuration interface and 3. completing-read metadata: I think it makes sense to offer all these proposed options - selectrum-default-config, selectrum-session-config and selectrum-metadata, depending on the use case you may prefer one over the other. I think @clemera already worked out many details on how things could be done.

  3. arbitrary minibuffer setup - I think no additional support for minibuffer hooks should be added to selectrum. Using minibuffer-with-setup-hook is sufficient to cover the relevant use cases. I am using that in consult to in order to implement narrowing and preview.

  4. some other thoughts - the question is if we prefer selectrum-variable-names or :keys? I am more leaning to use keys and @clemera is more leaning towards using variable names. But I do not feel strongly here.

@clemera
Copy link
Collaborator

clemera commented Dec 15, 2020

Thanks for the nice summary @raxod502!

deprecating selectrum-read

I agree that this would be a nice!

new configuration interface, arbitrary minibuffer setup, some other thoughts

I think of this by distinguishing between the following three cases:

  1. The user configures the default settings
  2. The user/caller wants to configure session specific settings of some code which uses completing-read.
  3. The author of an individual completing-read call wants to (pre)configure session specific settings

I think 2 should have higher precedence than 3, the author makes a good guess of what makes sense for the session but the caller might disagree or wants to have additional settings. I think in the optimal scenario the caller would be able to have a way to overrule the choices of the author one by one.

I would like the simplicity that Selectrum settings could be let bind around calls but I think that means that the inner most call decides how the session behaves. Like we have it now authors can set variables locally in minibuffer-with-setup-hook to override default settings and users or other callers can also use minibuffer-with-setup-hook to override those again. It is not beautiful but it is also the standard way how session specific minibuffer settings are applied in general and it has the nice side effect that the outer most caller decides what settings should be used and the other settings of callers down the line stack up. Relying on minibuffer-with-setup-hook also gives inner callers the ability to enforce a particular setting and don't let callers override it via the (:append ...) feature if that would be needed in some case (which should be rare but it would be possible at least).

With this considerations my recommendation would be to introduce a selectrum-config function which could allow users to specify session configurations without having to learn about minibuffer-with-setup-hook and local variables and to adjust commands without using advice. Settings specified this way would have highest precedence. For developers we could clearly state in the README how you should configure session specific settings via minibuffer-with-setup-hook and also allow specifying settings via metadata (we could check internally if the settings already have a local binding in which case we can refuse to override them).

@raxod502
Copy link
Member

Yeah, those are good thoughts. Your analysis of the limitations of the let interface is well-founded and I like the hierarchy of configuration precedence (1)-(3) that you have set out; it seems quite correct. I think what you propose seems reasonable. What we could do is introduce macros for both users and developers, so that nobody would have to use minibuffer-with-setup-hook if they only wanted to do the equivalent of a let binding (but also document how to do it with minibuffer-with-setup-hook). That way the interface for each of (1)-(3) could be similar, but we'd get the advantages of how you've proposed to do things (since these macros are really just syntactic sugar on top).

As long as the multiple ways of doing configuration can be mapped to each other in some way, i.e. the kind of data that goes into each one isn't totally different.

@clemera
Copy link
Collaborator

clemera commented Dec 20, 2020

Back when I was using ivy I often had the annoying problem that it wasn't possible to easily reuse configurations of existing code. I really like thal with the approach we are currently discussing this would become very easy. Maybe we could propose such a macro for Emacs itself? Setting variables locally in setup hook is something very common so maybe the Emacs devs would be okay with adding such a macro.

@minad
Copy link
Contributor Author

minad commented Dec 20, 2020

@clemera Sounds like a good idea in general - but I think the first step should be to have a working and sufficiently general version in selectrum. After that, the proposal makes sense!

@clemera
Copy link
Collaborator

clemera commented Dec 20, 2020

Yes we can use it internally first and then when we think it is ready and general enough we can propose it.

minad referenced this pull request in minad/consult Jan 5, 2021
Remove many consult-preview-* customizable variables.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants