-
Notifications
You must be signed in to change notification settings - Fork 344
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
feat(prompt-editor): Add new ProseMirror-based implementation #6272
Conversation
This change is part of the following stack: Change managed by git-spice. |
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.
Some feedback inline. I got 2/3 of the way through this but it is huge so let me send some feedback and look at the tests in a future iteration.
Let's be more helpful with comments and more consistent with lints.
A number of questions inline about the details of the triggering, etc.
@@ -0,0 +1,17 @@ | |||
.popover-dimensions { | |||
width: clamp(300px, 65vw, 440px); |
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.
One sore point I have with the existing editor is sometimes the popovers are wider than the sidebar. Does the vw units help us avoid this problem here?
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've copied this from the current implementation, so probably not.
However this new implementation uses a separate plugin for popover management, which includes logic to resize the popover to the available width of the viewport (which I assume is just the sidebar in an IDE).
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.
Yeah, the old version uses radix popover which doesn't set any strict boundaries for popover elements (like max-width, max-height) and also does not shift popover properly if there is no enough space between target and viewport boundaries, floating UI should solve the problem
inputRules({ | ||
rules: [ | ||
new InputRule( | ||
// Trigger on @, at beginning or after space |
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.
Would be great to test this with multiple lines.
Also, today I can write multiple at-mentions without a space between them (one is inserted, but is not required...)
Also, today I can go to the middle of my composition and start typing at-mentions, but the terminal $
is going to stop that (...unless it is interpreted as a multiline regex, in which case I can type them at the end of lines but not in the middle 🤷 )
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.
Also, today I can write multiple at-mentions without a space between them (one is inserted, but is not required...)
The new version will require a space or beginning of the line before the @
.
Also, today I can go to the middle of my composition and start typing at-mentions, but the terminal $ is going to stop that
The input for the regular expression is the some text before the cursor, i.e. the end of the text will always be the text before the cursor, not the end for the line or whole input.
From the docs:
The rule applies when the user typed something and the text directly in front of the cursor matches match, which should end with
$
.
if (view.state.doc.childCount === 1 && view.state.doc.firstChild?.textContent === '') { | ||
view.dom.setAttribute('data-placeholder', placeholderPluginKey.getState(view.state) ?? '') | ||
} else { | ||
view.dom.removeAttribute('data-placeholder') |
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.
Er... if I delete the text, shouldn't the placeholder come back? That's how the platform's placeholders work.
Or are these plugins pure functions or something, and ProseMirror is going to recreate the element and its data-placeholder
?
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.
When text is deleted then if (...)
is true
. That's because even when the input is empty, the document still contains an empty paragraph node. The else
branch is taken when the editor contains text.
) | ||
} | ||
|
||
function tooltipForContextItem(item: SerializedContextItem): string | undefined { |
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.
For future, we should make the context item define its tooltip.
0768e58
to
28a42d3
Compare
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 tested this manually and didn't find any big problems so approve to unblock. I tried the case when I switched on and then switched back to the older version and something is definitely going on there, some messages aren't picked up right with lexical probably because of some shifts in message schema (lexical state)
But this problem is ok for now, I suppose we still have to live with the exical state for a while in Cody Agent lexical state serialization. (Not this PR problem)
@@ -2,6 +2,7 @@ export { PromptEditor, type PromptEditorRefAPI } from './PromptEditor' | |||
export { ContextItemMentionNode, MENTION_CLASS_NAME } from './nodes/ContextItemMentionNode' | |||
export { MentionMenu } from './mentions/mentionMenu/MentionMenu' | |||
export { BaseEditor } from './BaseEditor' | |||
export { PromptEditor as PromptEditorV2 } from './v2/PromptEditor' |
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.
We probably should re-export this later independently so I could publish this package without a big amount of Cody Agent dependencies in order to use it in the Sourcegraph prompt library (can do this in the follow up PR though)
@@ -0,0 +1,17 @@ | |||
.popover-dimensions { | |||
width: clamp(300px, 65vw, 440px); |
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.
Yeah, the old version uses radix popover which doesn't set any strict boundaries for popover elements (like max-width, max-height) and also does not shift popover properly if there is no enough space between target and viewport boundaries, floating UI should solve the problem
28a42d3
to
806b534
Compare
806b534
to
17a9c08
Compare
I've made many of the requested changes; this is low risk because it doesn't affect any production code; the new editor will be opt-in via feature flag.
This PR adds a new version of the prompt editor, based on ProseMirror.
Video demo
sg-prompt-editor.mp4
Implementation notes
ProseMirror
We use ProseMirror with a custom schema, that only contains paragraph nodes, text nodes and mention nodes (the mention "chips"). Programmatic changes to the editor value happen via transactions, which are applied to the current state and produce a new state. More on that in the next section.
Main 'components'
The 'meat' of the input is modeled as an xstate statechart in
promptInput.ts
and consists of three main pieces:Initially I implemented each of these completely separately, but combined them all into a single statechart. While I normally prefer modularity, all of these parts have intricate connections, and all of them make the prompt input what it is, so it seemed to be reasonable to group them together. The nature of the statechart still makes them easy to reason about IMO.
I chose to implement the whole system as a statechart so that we have a mostly descriptive way to specify how the input should behave, in a central place.
Most importantly though, any operation that can change the value of the input is implemented as a state transition. This has the advantage that we can see in a single place which operations are available and how they will affect the input. And we can also easily test them.
promptInput-react.ts
is a thin wrapper around the implementation that makes the input and mentions menu data available via hooks and abstracts from the state machine.PromptEditor.tsx
renders the actual UI elements based on the state and provides the data from the 'outside world'. Specifically this component provides the implementation for fetching the mentions data.Auxiliary modules that support the above are:
MentionsMenu.tsx
- for rendering the mentions menu. It uses existing components for rendering the menu items.MentionsView.tsx
- for rendering the mentions 'chips'. I couldn't use the existing mentions node component because it was strongly depending on lexical specific data (but maybe that could be abstracted).lexical-interop.tsx
- makes it possible to use the new component as a drop in replacement (I hope) by converting lexical editor state to and from prosemirror state, based on my understanding of the lexical state that I saw in storybook. I didn't spend a lot of time testing this yet, but I'll iterate on this as needed.Styling
I'm not quite sure what current guidelines are around using CSS directly and using tailwind. I used tailwind classes mostly where I wanted to replicate the style of the existing input. Happy to eventually clean that up.
Differences to the current implementation
@mention
in the input@mention
's need to be explicitly triggered (like in linear). When you 'leave' and@mention
it turns into normal text.Things that I wanted to do but couldn't (yet)
Test plan
Manual testing and unit tests. This PR doesn't impact any of the code that is currently in use, it only adds the new component so that it can be used in the future.