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

Leaf Projectors #1218

Merged
merged 184 commits into from
Aug 7, 2024
Merged

Leaf Projectors #1218

merged 184 commits into from
Aug 7, 2024

Conversation

disconcision
Copy link
Member

@disconcision disconcision commented Feb 20, 2024

Overview

This PR is intended to start (but not finish) a system for flexible projectional code-editing UI. Right now, the system should be sufficient to serve as the basis for non-splicing livelits, type hold inference UI, hiding parts of code in live views (e.g. function bodies), and at least a basic form of module collapsing. Both inline and block (multiline) GUIs are supported, though there is only a single reference implementation for the latter (TextAreaCore), so some details might not have ended up on the right side of the projector server / projector client divide. I want to support a range of use cases so I'm solicited feedback on the flexibility of the interface.

Getting Started

  • See the Documentation/Projectors slide in the editor for a functional overview
  • The module signature for projectors is defined in ProjectorBase.Projector
  • The core logic is distributed across four modules: ProjectorBase, Projector, ProjectorPerform, and ProjectorView
  • The first two are divided only to avoid cycles. This induces a similar split of Zipper into ZipperBase
  • All of the above modules are documented, as are the projector actions definitions in Action.t
  • The implementations of the actual projectors are in modules suffixed with Core. These currently include FoldCore, InfoCore, CheckboxCore, SliderCore, SliderFCore, and TextAreaCore.

How the projector system interacts with the parent editor

  • The projector system covers bits of syntax in the parent editor with embedded GUIs. Projectable syntax is currently limited to convex pieces (both tiles and grout). (In the future, this can be ~ straightforwardly relaxed to any segment which is semantically self-contained: any segment which parses, including nullary parses like runs of secondary.)
  • A projection is created/removed by wrapping/unwrapping the underlying syntax with a new Piece.t type, Projector(Base.projector_kind), which stores a record entry specifying the kind of projector, the syntax itself, and an optional serialized model for internal projector state. Projectors have an associated UUID which is set to that of their wrapped syntax. When their wrapped syntax is transformed, the id for that syntax is reset to the projector id to retain sync. (This is mostly an implementation detail; a convenience to retain an association between a projector and the id-tracked semantic information if its wrapped syntax)
  • Whenever a projector needs to be seen or respond to a message, its kind is hydrated into a first class projector module of type ProjectorBase.Projector, whose various methods determine all projector-specific logic. Projector views have access to certain information fed to them by their parent editor, including their underlying syntax and static Info entry (later: dynamic info). Views are also provided with callbacks for both local (kind-specific) actions and parent editor requests.
  • MakeTerm unwraps (removed) projectors as the term is constructed, producing as a side-effect a map of ids to projectors which is used for the view.
  • For editor view purposes, when a Projector is encountered in code rendering, it is represented as a placeholder string consisting of only whitespace and linebreaks. The projector GUIs are drawn separately (using the projectors map generated above in maketerm) and are absolutely positioned with respect to the parent editor.
  • For editor move/select/edit actions, projector pieces are handled analogously to single-character monotiles with a fixed mold. This makes them roughly analogous to a hybrid between single-character monotiles and the whitespace piece type: movement/selection/deletion treats them as atomic units, except they have a fixed convex shape.

Caveats and Wontfixes

  • Currently only convex pieces can be projected, so many terms will have to be wrapped in parens to be e.g. folded. This will be relaxed in a subsequent PR
  • Projectors are persisted in the syntax, but are not preserved on actions which serialize to text like copy/cut/paste. This will be mitigated in a subsequent PR
  • Substantial adjustments affecting only one specific projector (that is, requiring more internal logic but not an API change) are probably outside the scope of this PR. I already have enhancement ideas for basically all the individual projectors here, especially Fold, Info, and Slider.
  • Substantial adjustments to the UI for adding/removing projectors is probably outside the scope of this PR. The keyboard shortcuts and projectors panel are intended to be basic generic invocation mechanisms; I expect that different genres of projectors will have custom invocation stories

Adding a new kind of projector

  1. Create a new core module implementing ProjectorBase.Projector (or copy e.g. FoldCore)
  2. Add an entry for it in Base.projector_kind (used for adding/removing/updating)
  3. Register the core module in Projector.to_module
  4. If you want to expose the projector via a keyboard shortcut, see the existing entry for Fold in Keyboard
  5. If you want to expose the projector in the projector panel bottom bar UI, update ProjectorView.name, ProjectorView.of_name, and ProjectorView.Panel.applicable_projectors
  6. If you want to manually manage the projector as part of the update cycle, see the implementations of the SetIndicated and Remove actions in ProjectorPerform for how to manually add/remove projectors from an editor

Other changes made in this PR

  1. Many editor-specific actions have been relocated from Update to Perform
  2. Statics storage and calculation has been moved to Editor (requiring settings to be piped to many modules)
  3. Adding projectors to Zipper induces a serialization change!
  4. Since the above broke some of the documentation slides (due to eg eval now being a reserved keyword), I updated these, in particular Basic Reference, with new language features

TODO(andrews)

  • View: Text layer: Render placeholder instead of projected term
  • View: Deco layer: Render projected UI over placeholder
  • Action: Move: Skips over the projected term
  • Action: Select: Selects through and over the projected term
  • Action: Indicated projections override keyboard handler
  • View: Deco layer: Indicated deco for projected UI
  • Action: Update syntax under projector
  • Bug: Projector statics update is delayed one action
  • Bug: Clicking on projector only seems to register if caret is on right side
  • Bug: Action: Can't properly fold more than one term in the same segment
  • Bug: Action: Movement into concave side of folded infix/prefix/postfix crashes
  • Bug: Action: Under some conditions moving/selecting past a fold in a nested context skips to end of program
  • Bug: In school mode, typechecking and eval are using display version of terms... see TODOs
  • Cleanup: editor state/meta
  • Deal with indication in a more principled way than the find_p hack
  • Core: Factor out projectors into first class modules
  • Action: Gate folding on tile shape (prevent nonconvex)
  • View: Folded term decoration should be hexagon shaped probably
  • Move positioning code outside specific view fns
  • Internalize actions
  • Add proper internal action type
  • Try to remove statics from action
  • Cleanup/rename syntax_map
  • Cleanup TODOs
  • Completion buffer doesn't show (it still works though, you can tab-complete blind)
  • In Exercises mode, the type info projector placeholder size is wrong
  • When drag-selecting, projector deco stays in indicated mode until you're entirely past it
  • langdocs selector UI has become misaligned (how)
  • redo type arrow precedence correction on polymorphism slide
  • indentation for last delimiter of case/lets is broken
  • When you undo textarea typing when textarea isn't focused, textarea content doesn't update (although the underlying syntax does)
  • No error hole dec lines
  • Tab-Completing "let" doesn't work
  • Click to move at end of line too enthusiastic

@cyrus- cyrus- changed the title Leaf Projectors WIP Leaf Projectors Mar 1, 2024
@cyrus- cyrus- added the in-development for PRs that remain in development label Mar 1, 2024
@cyrus- cyrus- mentioned this pull request Mar 1, 2024
src/haz3lweb/view/ProjectorView.re Outdated Show resolved Hide resolved
src/haz3lweb/Keyboard.re Outdated Show resolved Hide resolved
src/haz3lcore/statics/MakeTerm.re Outdated Show resolved Hide resolved
src/haz3lcore/statics/TypBase.re Outdated Show resolved Hide resolved
src/haz3lcore/zipper/Editor.re Outdated Show resolved Hide resolved
src/haz3lcore/zipper/ProjectorBase.re Outdated Show resolved Hide resolved
Comment on lines 130 to 134
/* Projector model and action are serialized so that
* they may be used by the Editor without it having
* specialized knowledge of projector internals */
type serialized_model = string;
type serialized_action = string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the Cook functor solution and am weighing up whether it might have applications to UI

@@ -12,3 +16,115 @@ let div_if = (p, ats, ns) => p ? div(~attrs=[ats], ns) : div_empty;
let span_if = (p, ats, ns) => p ? span(~attrs=[ats], ns) : span([]);

let unless = (p, a) => p ? Effect.Many([]) : a;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally these feel like view/Widgets.re things to me instead of util/Web.re things

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, i forgot about Widgets. maybe we should just combine these modules? do you see a distinction between them? right now i can't just move things to Widgets as it's in web (renegotiating what goes where is likely going to be an ongoing process now that the core/web division is changing)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me web is just like wrappers around html things and widgets has more hazel-specific logic and styles etc - maybe we move widgets to util/Widgets.re if it can't be in web? (I still find it a little concerning that we need an html definition of a slider in core)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some similar issues on my livelits branch. I struggle to see how the projectors can be web (platform) agnostic. So unless they're entirely configured in web I don't know how you'd excise the dependency from core.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Negabinary i started to move widgets but looking at it the current arrangement makes sense to me. the range and TextArea components i defined in web are intended to be completely independent from hazel logic. all of the textarea logic is just to get and set text area ui state; it could all be in vdom in-principle. otoh current widgets are all related to hazel UI chrome outside the syntax-editor as such. so these modules are disjoint along two different dimensions. admittedly thats not well-reflected in the naming

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(via the tangent "what does it mean to be truly view independent?" => "let's take string out of Core" => "let's string out")

src/util/Util.re Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TextArea has some weird behaviour when you type a speech mark into it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to confirm that you're seeing the same weirdness: quote characters get inserted fine in the textarea, but these actions are not reflected in the underlying syntax? (this also means the shape of the textarea doesn't expand for quotes) or are you seeing something above and beyond that?

i knew about this one but was ignoring it as i don't totally understand how to address it in a principled way. the way regular string literals work in the underyling syntax model is a bit ad-hoc; quotes have special-case behavior relative to all other characters. briefly: inserting a quote must automatically insert a closing quote; otherwise the text serialization of that code state has an ambiguous lexing. but we don't want to prevent someone from just typing an empty string normally. the solution we ended up with is that typing a quote inside a string simply moves the caret to the end of the string. not pretty but in practice seems to work fine. of course ultimately we want escaped characters in strings, but ideally this will all be handled at the core syntax layer.

could add some textarea-projector-specific code that catches keystrokes, suppresses quotes, and the refires the event to the textarea i guess. implementing the string livelit with the DOM textarea is likely more trouble than it's worth, but i wanted to do this as a trial run for embedding non-hazel-native components...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah same weirdness! Hazel afaik doesn't have any quote escape mechanism so in terms of the underlying syntax the best thing is probably just to suppress quotes for now if that's straightforward enough

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, suppressing quotes for now

Copy link
Member

@cyrus- cyrus- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks and feels great overall.

Only bug I can find is that when the projectors panel is disabled you can still click the grayed out button (and it seems to do some sort of folding):

image

Other notes in addition to a few inline comments in the code:

  • The copy-and-paste and undo issues you indicate as a wontfix for this PR, which I am okay with for now, but that I don't want to leave hanging too long. What is your plan for those?
  • The first scratch slide now has a bunch of projectors stuff in it, which should instead be under Documentation.
  • I was expecting to be able to fold when my cursor was on something like an ap, e.g. in the example above, where it would fold the entire highlighted term, but I guess that's related to the parenthesization issue that we're going to fix later?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: module naming in util is inconsistent, some end in Util and others don't.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the pattern matt has now established of importing util modules in Util so you can just refer to them as e.g. Util.Web; I think going forward we should migrate away from Util in the filename.

(write-file js-of-ocaml-flags-dev "(:standard)"))
(write-file
js-of-ocaml-flags-dev
"(:standard --debug-info --no-inline --pretty --source-map)"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this change the dev experience meaningfully?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not afaict. but it does make the flags the same across all our build targets, and the same as david's setup, so at least it eliminates that potential source of confusion

src/haz3lweb/Keyboard.re Outdated Show resolved Hide resolved
src/haz3lcore/zipper/ProjectorBase.re Outdated Show resolved Hide resolved
@@ -2,8 +2,8 @@ open Util;

[@deriving (show({with_path: false}), sexp, yojson)]
type buffer =
| Unparsed
| Parsed;
//| Parsed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's going on here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an artefact of separating the original TyDi implementation from the llm completion stuff. The parsed buffer option is currently meaningless so I just removed it until the LLM stuff is reimplemented

src/haz3lcore/zipper/projectors/CheckboxCore.re Outdated Show resolved Hide resolved
@hazelgrove hazelgrove deleted a comment from cyrus- Aug 1, 2024
@disconcision
Copy link
Member Author

commenting to remind myself about the slider keyboard input focus issue

@disconcision
Copy link
Member Author

  • The copy-and-paste and undo issues you indicate as a wontfix for this PR, which I am okay with for now, but that I don't want to leave hanging too long. What is your plan for those?

I've been hoping inspiration will strike for a simple approach to copy/paste. The options seem to be: Implement some kind of synchronization between an internal (segment) buffer and the browser copy/paste actions. This is probably not too bad, but I'm also just not totally sure if it's going to work seamlessly syntax-wise. Reinserting a segment as a selection should work (it's how pickup/putdown used to work), but i'm wary against since-then baked in assumptions around adding incomplete syntax. At the very least paste has to be able to replace UUIDs. Hopefully not that bad but it's not something I want to get bogged down in atm. The other option is implementing some kind of attributes system, and use those to represent projectors in the text representation. This requires some more planning, and would be natural to implement at the same time as text-based livelit invocation (not currently on my agenda, but would likely do at some point). Ofc this latter option doesnt have the advantage.

@disconcision
Copy link
Member Author

@cyrus- et al: all issues addressed I think. function applications don't try to project now (greyed-out toggle should be in-sync).

  • Ability to project terms which aren't single tiles will follow in a self-contained PR
  • Better Undo/Redo will follow at some point. I'll create an issue for now. Current state is: it works sensibly, but causes textarea to lose caret position
  • Copy/Paste will follow at some point. I'll create an issue for now

@cyrus-
Copy link
Member

cyrus- commented Aug 4, 2024

@disconcision tab-completing let/if/etc. doesn't work still, what's the issue there?

Also we talked about connecting error marks with the lines so it doesn't look like there are multiple errors, did you decide against that or just not done?

@disconcision
Copy link
Member Author

@disconcision tab-completing let/if/etc. doesn't work still, what's the issue there?

Also we talked about connecting error marks with the lines so it doesn't look like there are multiple errors, did you decide against that or just not done?

planning to fix both just taking longer than id like

@disconcision
Copy link
Member Author

@cyrus- all issues addressed. in particular, tab completions of definitions is fixed, and error holes now have child lines.

@cyrus-
Copy link
Member

cyrus- commented Aug 5, 2024

odd decoration on the end here:
image

@disconcision
Copy link
Member Author

@cyrus- technically that's already there for the term decorations, you just can't see if because the term shards are opaque. I looked briefly but I don't really know how to address it in the current approach to line drawing

@cyrus-
Copy link
Member

cyrus- commented Aug 5, 2024

@disconcision maybe make the error shards opaque as well to avoid the issue for now and @dm0n3y can take a look in the next revision of the decorations?

@disconcision
Copy link
Member Author

@cyrus- i've addressed the error hole thing in summer-refresh; tedious to replicate here as the css is completely different. i've made the background color of the error holes the same color as the code background + the red overlay, so it looks the same. the cost is that this means we need to either move the error holes behind selections or it looks super funky. but that's not a huge issue i think

@cyrus- cyrus- merged commit 5cb2cf2 into dev Aug 7, 2024
2 checks passed
@cyrus- cyrus- deleted the projectors branch August 7, 2024 21:15
@disconcision disconcision removed the in-development for PRs that remain in development label Aug 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants