fjvallarino/monomer

Programmatically controlling TextArea content

Opened this issue · 10 comments

Hey. I'm looking into controlling the TextArea widget programmatically. Think, Markdown-aware widget that wraps selected text with asterisks when Ctrl-b is pressed: **text**, etc..

I saw there was no way to do this right now. Would you consider exposing interaction with the widget to programmers?

Hi @klausweiss!

How do you envision it could work? I'm thinking about two possibilities:

Events

  • Provide an event generated by textArea that reports when the user changes a selection (similar to onChange, but with more information). This will give you the context regarding where the action should be performed.
  • Provide a message that can be sent to textArea with the new text to replace the current selection. Of course, you can always update your model directly.

Going this route allows you to use the existing mechanisms for handling any other kind of event. It also allows you to perform IO actions before setting the new content.

Hooks

Provide a function that is called directly by textArea when an event happens. This function needs to be pure because event handling in widgets is also pure (you can launch a WidgetTask from a widget, but it will run after event handling is complete).

In the case you mention, this method would probably be easier to use, although it's perhaps more complex regarding changes to the widget.

Actually, ability to get the current cursor location would enable doing similiar things to an extent. For instance, instead of selecting text -> ctrl+b -> wrap them with **, with the cursor location, one can do, put the cusor on/inside a word -> ctrl+b -> wrap the word the cursor is on with **.

If we want, the cursor location can also be used for info about current selection if the location info contains the starting position (start_pos) and ending position (end_pos). If there is a selection, then obviously, start_pos and end_pos will be different, otherwise, i.e. if it is same (start_pos == end_pos), then it isn't a selection, but a "normal" cursor location.

Sure, absolutely. The status event would provide cursor information even if there's no selection (that's how textArea works internally). It could also provide the already split text lines and their corresponding glyphs. The "replace" message would also act as an insertion mechanism if a selection does not exist, etc.

What happens with those events is something that the user of the widget will implement. The textArea itself will not handle Markdown, word splitting, nor decide what happens with the keystrokes; it will continue to be a bare-bones widget,
equivalent to HTML's textarea.

I only have this much experience with monomer, but out of the two solutions you proposed, I found the events-based one more idiomatic.

How about

  • an event generated by textArea that reports current cursor state, as you both suggested,
  • and a separate event that you can send to a textArea instance (through Message) to change the selection?

I just thought of yet another way, which would allow for easy expansion in the future. It's a simple solution, like the others proposed in this thread.

We could have textArea operate on a different type of lens. Instead of just Text, it could be a more complex structure that comprises of both Text and the selection information. For ease of use (and backwards compatibility) there could be a separate constructor with a simple Text lens (like we have right now) and a complex one with a TextAreaState of sorts. That's where the ease of expansion lies - TextAreaState could comprise of more fields in the future.

I have thought a similiar one - as in general solution, but was doubting whether it would be overkill. My idea is to do what modern editors and related tools do for reporting changes:

  • An enhanced version of onChange that would report each new change.
  • Text would be stored in [Text] (a list of lines -- split at newline chars).
    • "list" is meant to be figurative, it could also be in, for instance, a Seq.
  • Changes will be reported using an ADT which will have seperate constructors for insertion, deletion, and maybe replacement too (for simplicity / ease of use), along with a position (row, col) or a range (row_start, col_start) (row_end, col_end).
  • (Optional) For ease of use, for insertion, only send the event when a newly entered char is in a pre-defined set of chars (e.g. a union set of whitespace chars and ASCII punctuations) (i.e. Monomer will send a cumulative insertion change, a la Monoid). If done this way, we'll need to take care of edge cases such as text field going out of focus after some text is inserted, but before a char in the set is inserted.

What @klausweiss suggested could work well.

One important thing is to remember that some widgets have internal state, and receive/report values which may be of a different type (this is the case of textArea). The internal type of textArea needs to exist as its own entity and should be known in advance by the widget. For this reason, we will need some to convert from/to the user-provided type. If the user wants full access to the internal state, they can provide id as the conversion function. In case they want Text (current scenario), we can apply the current logic of splitting/joining as appropriate. This conversion could be either regular functions or virtual lenses. I think regular functions are more straightforward, but at the same time I doubt there will be more than 2-3 conversions (TextAreaState, Text, and maybe String).

I will think about this solution and give it a try once I finish a few ongoing things (popup and colorPopup widget, plus documentation improvements). It may be a while until I get to it.

My only concern with exposing the internal state is that it opens up a door to problematic upgrades since users will depend on a data structure that was meant to be for internal use by the widget.


I still think the events/messages solution could work well and sounds simpler to implement. While I don't intend to turn textArea into a complex editor because I just don't have enough time to maintain it (it would be a large project in its own right), several things could be improved in textArea. Text handling is done in a performance-naive way, and glyph position recalculation should be restricted to only the sections that changed. For example, using text-rope might be a good idea, although I'm always careful about adding dependencies. The main point is that keeping the internal state hidden allows for more flexibility to improve things without breaking users' expectations.

I could get around to doing this relatively shortly; I'll keep you posted.

While I don't intend to turn textArea into a complex editor because I just don't have enough time to maintain it (it would be a large project in its own right) [...]

I understand. I suggested it becuase you said that the widget already keep a list of lines, and you don't mind exposing cursor state. With both of them, it looks like it is possible to send events for each new change with some work. You can simply limit the change-events to insert and delete, and leave the rest of the processing to user (of this library). If it is relatively complex, it's probably better to just avoid this.

In case you misunderstood what I said above: I don't want monomer itself to provide full editing capabilities, but rather the basics of basic building blocks that users can build upon on, which IMHO, Monomer already provides, except the cursor state, and a better change-events reporter.

@MuhammedZakir also sorry I was not clear. I do intend to support the events/messages you mentioned; they are mainly possible with what already exists, or the required changes do not take a lot of effort.

What I meant is that I prefer not going much beyond that in terms of functionality because a text editor is a complex piece of code, with a huge list of features that users may want to have. Supporting such a widget would make it hard for me to focus on what I think the project needs (a flexible core, good documentation, and mobile support).

I will think about this solution and give it a try once I finish a few ongoing things (popup and colorPopup widget, plus documentation improvements). It may be a while until I get to it.

Wow, that's awesome! Thanks!

My only concern with exposing the internal state is that it opens up a door to problematic upgrades since users will depend on a data structure that was meant to be for internal use by the widget.

I don't think we're on the same page (I wouldn't thought of a user-provided type), but I will use your judgement. Happy to have an impact on such a great library! (: