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

feat(prompt-editor): Add new ProseMirror-based implementation #6272

Merged
merged 18 commits into from
Dec 21, 2024

Conversation

fkling
Copy link
Contributor

@fkling fkling commented Dec 6, 2024

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:

  • The editor itself, whose state gets updated either via user interaction or programmatically, via events.
  • The data fetching for the mentions menu, including which provider is currently selected.
  • The mentions menu itself, specifically its visibility state and which item is selected.

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

  • Renders a decoration around the @mention in the input
  • @mention's need to be explicitly triggered (like in linear). When you 'leave' and @mention it turns into normal text.
  • Currently does not memoize results from providers. I first need to understand better why/when/how we want this. Given how it's implemented it should be easy to add though.
  • Some difference regarding styling and copy for the mentions menu.

Things that I wanted to do but couldn't (yet)

  • I really wanted to provide a better experience for the mentions menu while fetching the mentions data (e.g. showing a loading indicator). But given the nature of the API for fetching the data this was not easily doable. It's something I want to investigate when working on the new designs for the menu.
  • Eventually we want to use the input also in the prompt library for creating prompts. For this we might want to introduce a new node type for placeholders. I don't have enough context on that project yet so I didn't do anything in this direction, but adding support for this shouldn't be too difficult.
  • There is no support for more complex node types like lists or code blocks. I think we want to add those eventually but I also need more information on this first.

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.

@fkling
Copy link
Contributor Author

fkling commented Dec 6, 2024

@fkling fkling self-assigned this Dec 6, 2024
@fkling fkling marked this pull request as ready for review December 6, 2024 15:32
@kalanchan kalanchan requested a review from a team December 6, 2024 22:56
Copy link
Contributor

@dominiccooney dominiccooney left a 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);
Copy link
Contributor

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?

Copy link
Contributor Author

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).

Copy link
Contributor

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

lib/prompt-editor/src/v2/MentionsMenu.module.css Outdated Show resolved Hide resolved
lib/prompt-editor/src/v2/MentionsMenu.tsx Outdated Show resolved Hide resolved
lib/prompt-editor/src/v2/MentionsMenu.tsx Outdated Show resolved Hide resolved
inputRules({
rules: [
new InputRule(
// Trigger on @, at beginning or after space
Copy link
Contributor

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 🤷 )

Copy link
Contributor Author

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')
Copy link
Contributor

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?

Copy link
Contributor Author

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.

lib/prompt-editor/src/v2/plugins/readonly.ts Outdated Show resolved Hide resolved
lib/prompt-editor/src/v2/promptInput-react.ts Outdated Show resolved Hide resolved
lib/prompt-editor/src/v2/promptInput-react.ts Outdated Show resolved Hide resolved
)
}

function tooltipForContextItem(item: SerializedContextItem): string | undefined {
Copy link
Member

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.

Copy link
Contributor

@vovakulikov vovakulikov left a 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'
Copy link
Contributor

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);
Copy link
Contributor

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

@fkling fkling force-pushed the fkling/prompt-editor branch from 28a42d3 to 806b534 Compare December 21, 2024 12:57
@fkling fkling force-pushed the fkling/prompt-editor branch from 806b534 to 17a9c08 Compare December 21, 2024 12:57
@fkling fkling requested a review from dominiccooney December 21, 2024 13:06
@fkling fkling dismissed dominiccooney’s stale review December 21, 2024 13:08

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.

@fkling fkling merged commit abf27bb into main Dec 21, 2024
38 of 42 checks passed
@fkling fkling deleted the fkling/prompt-editor branch December 21, 2024 13:09
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