-
-
Notifications
You must be signed in to change notification settings - Fork 377
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
RFC: Behavior #64
RFC: Behavior #64
Conversation
state/src/behavior.ts
Outdated
if (defaults) for (let key in defaults) | ||
if (result[key] === undefined) result[key] = (defaults as any)[key] | ||
return result | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think combineConfigs
should rather be a stand-alone function. Behavior
already has two different kinds of static properties: The define(Set)
factory functions and the stateField
/ multipleSelections
/ viewPlugin
/ indentation
well-known Behavior
s, and combineConfig
doesn't fit in there and confuses me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These kind of utility functions are always hard to find a good place for. It's somewhat dubious whether this belongs in state at all, but I needed it several times for the existing plugins, so I guess it's nice to provide it. I'm okay with moving it to a stand-alone function. Our doc generation tool should be flexible enough for us to be able to put item docs in sections (as with builddoc's src/README.md system), so then we can still make sure that function appears near Behavior
in the docs).
state/src/behavior.ts
Outdated
Behavior.viewPlugin = Behavior.defineSet() | ||
Behavior.indentation = Behavior.defineSet() | ||
|
||
export class BehaviorSpec { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name confused me: I would expect a SomethingSpec
to be an abstract thing with which you can create Something
s, but Behavior
and BehaviorSpec
are the other way round?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Naming remains hard. Any suggestions? BehaviorInstance? BehaviorUse?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we could just switch the names, but (the current) Behavior
will be used much more than (the current) BehaviorSpec
, so I think calling that BehaviorUse
is better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, using Behavior
for the main point of interaction was a pragmatic thing. I'm okay with BehaviorUse
state/src/behavior.ts
Outdated
get(state: EditorState): A<Spec> { | ||
return state.config.behavior.get(this) || none | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe consider removing this subclass and move the check to the callers (currently commands and editorview). Or instead generalize a default value concept for non-set behaviors, too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also consider having this class a bit ugly, but the alternatives seem more problematic. For non-set behaviors, being present or not are distinct conditions, and thus we'd require some kind of special null value there, not a default.
Maybe make get
error when called on a non-set behavior that isn't present, and provide a different accessor to check for availability?
Thank you for your input! Does the overhaul as a whole look like an improvement to you? Another wart that I'm not happy with is the term 'set' (as in |
I've implemented the solution to get rid of |
That looks nice. To be honest, I was just playing around with a different direction: I noticed that As for merging, I'm still playing around with this to better understand it. So far I've mostly looked at the implementation, not usage. I'm still quite confused by the terminology and the fact that behaviors are used for implementing both plugins and small composable aspects of plugins. That's elegant, but difficult to wrap my head around. I'm quite confident that we will merge this and move in this direction, but I would like to play with it a bit more and maybe rename or move around things. If it makes your work easier we can merge it and continue on master. |
I pushed my branch because I think I really like the way the two types of |
|
(Think of this as |
Maybe we can look at smaller steps first: What do you think about moving the I'll try to focus on the user-facing side of things now to provide more useful feedback. |
I think a lot of my initial confusion can be solved by renaming things:
|
I guess I should have written up proper docs (and comments) before creating this pr—was so glad to finally have something that worked that I just had to rush it onto github.
Yes, they were called dependencies before, but that doesn't really capture it (these are behaviors injected by the parent behavior, which is something quite different from what dependencies in module systems tend to be).
At one point I had a design where plugins and behaviors were different, so that plugins were the top-level things you included and each could provide any number of behaviors, but that seemed both too restrictive (you'd also need dependencies on plugins to do things like plugins including keymaps) and introducing more concepts than necessary (in the end there's no fundamental difference between them, and it's often convenient to abbreviate a simple plugin as just a behavior). I'm okay with |
I think it's alright for me to figure things out this way. It's really the flexibility that gets me: In history, behaviors implement a factory with config merging and one dependency; In keymap and legacyModes, it's just mapping one config at a time to a set of behaviors; gutter, matchBrackets and specialChars merge config and then evaluate to a single viewPlugin; multipleSelections too but also adds a marker behavior. That makes it really difficult to find names for concepts or even understand the concepts in the first place. |
Are there use-cases where you would want to get a behavior not only based on the type, but also spec? |
Does it make sense to even store the value of behaviors that only emit configuration? Currently you only get the values for indentation, history, viewPlugin, stateField and multipleSelections. Those are pretty obvious and I cannot think of a reason you would need the others that are currently available. |
(Just throwing thoughts and questions around) Quick overview over the currently defined behaviors:
|
I don't think so.
No. My thinking is that it's mostly harmless to allow this, and it helps keep things simple, but this may be a pointer in the direction of a design change. Especially since, if the |
|
Sure, but why add another mapping when a private field suffices? |
To make |
I think the most helpful change for me would be to make it explicit when a ›behavior‹ provides, uhm, behavior itself as compared to only specifying subbehaviors. |
(I think I generally prefer comments to additional code, when it comes to making the program easy to understand — at least those won't have to be loaded and evaluated by every user.) |
I find that picking things apart (at least in my mind) that don't necessarily belong together helps me finding better approaches. |
Thinking about this one concretely, those known sub-behaviors have to persist, so I don't know where they'd be stored if local to resolve. In a module-global variable? |
Persist between what? Multiple resolve calls? |
Yes. |
So (naming is just a sketch) if we would have something like: keymap = Behavior.usages<Keymap>({
behaviors: configs -> [viewPlugin.use(/*...*/)]
})
gutter = Behavior.singletonUsage<GutterConfig>({
combineConfig: configs -> config,
behaviors: config -> [viewPlugin.use(/*...*/)]
})
stateField = Behavior.define<StateField<any>>()
history = Behavior.defineSingleton<HistoryConfig, HistoryBehavior> {
combine: configs -> config,
create: config -> HistoryBehavior,
subBehaviors: HistoryBehavior -> [stateField.use(/*...*/)]
} … that wouldn't be as elegant, but – provided we find good names for the factory functions – easier to understand. |
Not that I think this discussion is particularly relevant, but storing the known sub-behaviors across multiple resolve calls is just an optimization, right? |
(Yes, it is.) |
So that it can be properly typed. (This was an issue we had with the old plugin system that was accidentally fixed by behaviors!)
And have them explicitly mark their target type in a type parameter, in preparation for using these in view plugins.
To reduce the number of concepts client code has to import, and to make things that can be plain functions actual plain functions.
I've rebased this on top of the current master branch, and made some more adjustments. The idea of these was to introduce less concepts and types (see #67). Basically, I've made it so that consumers can import a single class, and have everything they need (except for those that store a behavior store, those need to also import The biggest change is that everything that can be a raw function is now a raw function (behaviors and extensions), without a custom type slapped on top of it. Plain, non-unique extensions can even be written as plain functions (i.e. Instead of putting the |
Although I usually like my type system to do the work it's supposed to do, I agree that we need some dynamic type checks where we interface with (possibly non-TS) user code. |
gutter/src/index.ts
Outdated
import {EditorView} from "../../view/src" | ||
import {StateExtension} from "../../state/src" | ||
import {combineConfig} from "../../extension/src/extension" | ||
import {EditorView, viewPlugin} from "../../view/src" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import {EditorView, viewPlugin} from "../../view/src" | |
import {EditorView, PluginView, viewPlugin} from "../../view/src" |
gutter/src/index.ts
Outdated
export const gutter = StateExtension.unique<GutterConfig>(configs => { | ||
let config = combineConfig(configs) | ||
return viewPlugin(view => new GutterView(view, config)) | ||
}, {}) | ||
|
||
class GutterView { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class GutterView { | |
class GutterView implements PluginView { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense, but PluginView is on the way (already is, locally) to be replaced by several view behaviors, so it doesn't matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, how does gutter
look like in that version? Currently, it's confusing that viewPlugin
's argument is allowed to return some arbitrary class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this:
export const gutter = ViewExtension.unique<GutterConfig>(configs => {
let config = combineConfig(configs)
return ViewExtension.domEffect(view => new GutterView(view, config))
}, {})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the 'arbitrary class' thing is still there, but that's how interfaces work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And domEffect
doesn't expect an interface GutterView
can implement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the question... it does exactly that, which is why that code typechecks, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, I see, you want implements
declarations. Yeah, you could add implements DOMEffect
on the class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's good :) In the current (old) version, it's obvious that we are following the framework's (hu) interface, but in the new, more terse version, I don't immediately recognize that.
} | ||
|
||
function getIndentation(state: EditorState, pos: number): number { | ||
for (let f of state.behavior.get(StateExtension.indentation)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think indentation
would be nicer as a standalone import.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? Does this go for all of the static values on StateExtension
? (Would it make sense to continue this discussion in #67 ?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it goes for all of them. Maybe because they are not extensions, but behaviors? I think indentation
should be indentationGetter
or getIndentation
or indentationAt
and they could all have a Behavior
suffix for clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point, behaviors can be considered to be a type of extension.
These are not getters—if you call them, you create an extension that holds the value you pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, they are not getters, but the behavior provided by indentation
is »getting the indentation at a specific position«. state.behavior.get(StateExtension.indentation)
returns an array of functions that return the indentation at a specific position.
history/src/history.ts
Outdated
} | ||
|
||
export function redoSelection({state, dispatch}: {state: EditorState, dispatch: (tr: Transaction) => void}): boolean { | ||
return historyCmd(PopTarget.Undone, ItemFilter.Any, state, dispatch) | ||
const historyBehavior = StateExtension.defineBehavior<HistoryBehavior>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is confusing. Maybe we can have a simple wrapper like this:
Extension.adhocBehavior = function<Value>(value: Value): Extension {
let behavior = Extension.defineBehavior<Value>()
return behavior(value)
}
EDIT: Wait, you still need to be able to retrieve the behavior, right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it confusing? And indeed, that wrapper would define an anonymous behavior that you can never access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's rather HistoryBehavior
that confuses me and I just wrote a comment over there. For the other, exported behavior types, this method is not confusing.
history/src/history.ts
Outdated
export function undo({state, dispatch}: {state: EditorState, dispatch: (tr: Transaction) => void}): boolean { | ||
return historyCmd(PopTarget.Done, ItemFilter.OnlyChanges, state, dispatch) | ||
} | ||
class HistoryBehavior { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class is mainly a handle so that the commands and depth getters don't have to close over the configuration (and hence, field). I think the code would be easier to understand if this were either
- split in two behaviors (
HistoryCommand
,HistoryDepth
) or - reduced to only storing the two fields with the content of the methods moving down into
cmd
anddepth
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pint 2. is a good idea. 1. seems like it wouldn't help much while introducing another separate element. Renaming the class to HistoryContext
or so might cover its purpose relatively well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See fc5b290
This definitely needs some getting used to from my side and there's probably a lot more changes that will happen, but I think we are at a point where merging would simplify our work. |
I agree on merging soon. I'm close to finishing the view behavior part (I hope), which has motivated several adjustments to this stuff already. I think when that works, I'll merge and we can address further issue with separate discussion/prs. |
I've merged this + view behavior into master. Some notes about the new code:
|
This branch contains an implementation of a mechanism that replaces (state) plugins. It works roughly like this:
get
method)Value=Spec[]
)The main things that this tries to accomplish is:
The approach is a little baroque (though I already put a lot of effort into keeping it minimal), but it seems to meet these goals well. I have, for example, introduced an indentation behavior that the legacy modes now provide, along with generic commands that do indentation, which use this behavior. I haven't tried to wire up tokens with bracket matching yet, but that should be able to work the same way.
I'd love to hear your comments, @adrianheine