In this chapter, we're going to add AI features to our code editor. This will include implementing the ghost text suggestions, handling tab key suggestion acceptance, creating the command or control plus k quick edit model, adding a fire crawl scraping functions so users can paste URLs into those quick edit models and we're going to add all other selection based code editing features. It's better to show you exactly what I mean. So this is the final product. And you can see that if I attempt to write my own on click method, I have AI giving me auto completion.
And besides that, I am able to use quick edit and give this, for example, a prompt, rename this to on enter or anything like that. And you can see it gets the job done. So that's going to be the purpose of this chapter. We also have some things from the previous chapter from CodeRabbit. So we're going to start by just fixing those issues such as some memory leaks and then we're going to go right into building the AI features.
So for now you can just have your npm run dev running and npx convex dev running. Let's go ahead and refresh our localhost 3000. The first thing I'm going to do is I'm gonna go inside of my source app folder, globals, and I'm gonna remove select none from here simply because I don't know how this behaves in other browsers and I don't want to give you some bad advice. It's better to have that not here, especially in global CSS. Later we can add it to like specific elements, but in global body element, maybe not the best idea.
And the second problem is we have no cleanup for our debounced updates. So that is inside of the editor view, specifically talking about this timeout ref. So every time we call on change, we debounce using the timeout ref. And we do clear it if two of them appear at the same time, but we never clear it if this unmounts. So that's something we should do.
We can do it very easily using useEffect. So down here I'm just gonna do a useEffect. Clean up pending debounced updates on unmount or file change. So if active tab ID has changed we're going to have a different unmount function here. If we have timeout ref, let's just clear it.
And make sure to import useEffect from React. That's it. That's all we have to add in our editor view. Perfect. Now let's go ahead and let's implement a new extension called Suggestion.
So for that I'm gonna go inside of Features, Editor, Components and I'm gonna go inside of the Code Editor. And in here we have to add our own extension. So I'm gonna go ahead and do that right here and it's gonna be called a suggestion. And I'm going to pass in the file name here. Now let's go ahead inside of extensions Let's create a new folder, Suggestion.
And inside of Suggestion, create an index.ts. It will have multiple files inside, so that's why I'm creating a folder. And now we have to go ahead and build this. So I'm gonna build this in stages so it's easier to understand. We're gonna start with stage one.
Stage one is basically gonna be static ghost text. It's going to teach you how to implement a basic widget type, decoration, state field, key map, and it's always going to show the exact same suggestion. So no fetching, no AI just yet, because that's too much information at once. So let's start very, very slow. So obviously, I have to create some kind of function which accepts the file name.
So let's do that. That's easy. ExportConstSuggestion, which accepts a file name. And let's go ahead and return an array. And now in here we're going to build three different, let's call them mini extensions, right?
The first one will be the suggestion state. Basically that's going to be our state storage, right? What's the current suggestion? And now let's go ahead and build it. So in order to build that, we actually have to add some imports from CodeMirrorState.
Let me go ahead and expand this here. State effect and state field from CodeMirror forward slash state. And I don't know if you've noticed, but we actually have access to a bunch of CodeMirror packages that we never explicitly installed. If I search for CodeMirrorState, it doesn't exist. And I've actually noticed that in the previous chapter, because we added this extension called CustomSetup, and I just pasted this entire thing and I've noticed I'm not getting any errors for this and that's when it clicked.
Oh, it's probably because we have CodeMirror installed and that's serving as kind of the base package. You can confirm that. So we should both have CodeMirror installed, right? And if you actually go inside of node modules and specifically search for CodeMirror here, and in its package JSON, you can see it maintains all of these dependencies so in case you're wondering how come we didn't have to install any of this is because we have all of them So I think we actually don't need to have code mirror view or code mirror commands, right? Because if I search for it, you can see it's one version here and then another version here.
So that's actually conflicting. So what I'm actually going to do is I'm going to uninstall CodeMirror commands and I'm going to uninstall CodeMirrorView simply because I can see that both of them are maintained in here. I think all other ones are actually good, right? None of these seem to appear in here, especially not the replit ones. So just for sanity check, I'm now going to search through my code for all the places where I import code mirror commands.
So one of them is inside of code editor where I import indent with tab. And you can see in here, it works perfectly fine. It's still inside of my node modules because it was maintained by another package, right? So, looks like that is working perfectly fine. If it's not working for you, you can always manually do npm install and just have it installed.
That's actually what I did in the initial build of this project. But understanding the code more now, I can see that all of these dependencies are actually maintained here. So I'm not sure it makes sense to install them separately when all of them live within this base code mirror configuration here. All right, so now that we solved that mystery, let's go back inside of our suggestions index here. So I've just imported CodeMirror from state and while I'm here I'm also going to add the following imports from code mirror view decoration, decoration set, editor view, view plugin, view update, widget type, and key map all coming from code mirror forward slash view.
So let's start by building something called setSuggestionEffect. SetSuggestionEffect uses state effect and we simply define it first. So it can be a type of string or it can be null. I'm gonna add a little comment here so you know what this is. State effect is a way to send messages to update our state.
Think of it like an action in a reducer if you've ever used Redux. And we define one effect type for setting the suggestion text. Now the second thing we have to do is we have to define our suggestion state. And that's going to use the state field.define. Again, it can be string or null.
So I'm gonna add some more comments here so it's easier to understand what that does. State field holds our suggestion state in the editor. The create method, which we're going to build, returns the initial value when the editor loads. And the update method is called on every transaction. Transaction is basically things like keystroke, cursor changed, I mean cursor position like this.
Right? Every time that happens, we're going to call the update method and we will recompute what the AI should suggest to potentially update the value. So let's start with the create method because that's the simple one. And in here, for now, all we're going to do is just add to do implement this because right now we don't really have anything functional, right? We don't have any endpoints to call.
And in the update we have value and transaction here. And in here what we're gonna do, we're going to check each effect in this transaction if we find our set suggestion effect we're going to return its new value otherwise we're going to keep the current value unchanged and we're going to do that using a for loop. For effect of transaction.effects, if effect is setSuggestionEffect which we have defined above, simply return its value. Otherwise, so outside of this for loop, return value. And let me go ahead and fix the typo.
There we go. So you shouldn't have any errors in the suggestion state nor in the setSuggestion effect. So that is the simple one done. You can see suggestion state is now fully functional here. Now, the second one we have to add is the render plugin.
So what's that going to serve? We currently implemented the state management for our suggestion text, and we implement what's going to be in future logic to update the current value, right? So if I put my cursor here, AI will give me relevant information about this. But if I change my cursor here, it's going to give me relevant information about this. That's what this is doing.
And this basically just holds the state. But we don't actually render this anywhere. So that's what we are doing now. We need our render plugin to render the ghost text. So let's go ahead and build that.
Const renderPlugin uses ViewPlugin from class. And in here we have to define a class, we have to give it decorations, decoration set. So everything that I'm adding here, decoration set, viewPlugin, we've added all of those inputs here so make sure you have all of them. Great. We now have decoration set.
Now let's go ahead and build a constructor that's going to initialize this .decorations which we've defined above. And it's going to use a build method which doesn't exist yet, we're going to create it and make sure to have the proper props here. Now, besides the constructor, we're going to have our update method here which is going to accept a view update. So basically what this function will do is it will rebuild decorations if the document has changed, cursor has moved, or the suggestion has changed. So this one makes sure that a new AI suggestion is being created.
And this one ensures that it's being shown again. So it's not stale, right? So that's the difference. This is the render plugin and this is the state management plugin. That's why I'm separating them.
All right, so let's see if the suggestions have changed. We are going to go through update.transactions.sum. Find the transaction inside and I'm immediately returning transaction.effects.sum, find the effect and immediately check effect is setSuggestionEffect. All right, now that we have that we are going to finally check if update.document has changed or if update selection set, or if the suggestion from the AI model is different. In any of those scenarios, we have to display something new to the user, right?
So if I move the cursor here, I have to update the render plugin. If I move it here, I have to update it again. If the AI build something new, I have to update it again. If I write something, I have to update the suggestion again. So all of these scenarios are covered here.
I just hope this is not confusing you. I'm not a big fan of this early returns, right? So let me try and kind of rework this so it's easier to look at. So basically like this, suggestions change, let me try to expand it even more. Suggestions change, update transactions, some transaction and then you can see I'm now opening a function, return transaction.fx.sum, effect, open a new function, return effect is set suggestion effect.
Right, so this or this, same thing. I just kind of feel like this is easier to read. I always find these very confusing. So whichever one you prefer this or this same thing. All right.
So now that we have that, Let's go ahead and do one more thing. Instead of adding all of these in a big if clause, we can kind of separate them in a constant, right? So should rebuild and then the exact same thing that we have in our if clause and then we can just use should rebuild here. So should rebuild basically means rebuild the decorations if the document has changed, cursor has moved or suggestion itself has changed. I feel like this is easier to understand.
I know this is a very new syntax. I doubt any of you was building code mirror extensions. So I'm trying to make it as primitive as possible so it's easy to follow along. I don't want you to just blindly follow me. I want you to understand what we're doing.
So that was the update method and now we have to build this build method that's currently underlined red. So let's build. We get the view which is a type of editor view and what we have to currently do is get the current suggestion from the state so const suggestion view state field suggestion state which is our state from above if there is no suggestion return decoration dot none And now what we have to do is we have to create a widget decoration at the cursor position. So first let's get the cursor. You can do that using view state selection main head.
And then return decoration.set. So now we're going to display something. And that's basically going to be a decoration, whoops not an object, an array. Decoration dot widget, pass in the widget, new, suggestion widget and pass in the suggestion. Now we also have to build the suggestion widget.
That's one thing I've missed, my apologies. And it's gonna be super simple. Basically, We can also write this directly here, but I just want to keep it separate. What we're doing now in the build here is just telling the decoration, okay, now finally show this to the user, right? User should now see this.
So let's keep this red for now and let's just pass in side to be 1. So what does side represent? It basically means render this after cursor, not before, which would be minus one, right? So if my cursor is here, this is where I expect my AI suggestion to arrive. And let's also chain .rangeCursor.
So this should be the only red underlined thing here. And we're not just done yet, so after this last curly bracket right here open another set of objects add decorations get plugin and execute plugin dot decorations and this basically tells the code mirror to use our decorations. All right, now let's go ahead and let's build the suggestion widget. So I'm going to do that right above our render plugin here. So class suggestion widget extends widget type.
And in here let's simply define a constructor read only text and call and execute super. Then go ahead and call to DOM. So what are we writing to DOM? Well, an element, a span element, and the span text content will be whatever we pass along from the build function. And you can see that that is basically going to be the suggestion, right?
So whatever we managed to extract from the suggestion field we're now going to add to this span element. Now let's go ahead and make this ghost text appearance by giving it an opacity. Let's make sure this doesn't interfere with clicks so it has the real kind of unobstructed feeling and let's return a span just like that and we can add some comment at the top of the class widget here. Widget type creates custom DOM elements to display in the editor. To DOM is called by CodeMirror to create an actual HTML element.
There we go. And now let's go ahead and yes, the file name is still unused because we're gonna need it later. Let's actually go inside of components code editor here and let's import suggestion from extensions suggestion. And let's try it out. So don't expect too much, right?
But it should, when you hover somewhere, so... Let me find something where I have some code. Wherever your cursor is, you should have hardcoded to do implement this. And it shouldn't interfere at all. But you can see how it re-renders exactly where I put my cursor or whenever I type, right?
So that's what all of those updates are doing. They are keeping track that we have a new display of our ghost text suggestion. Obviously right now it's not very intelligent, right? It's just going to add this everywhere. But I wanted to introduce you to building extensions through this very primitive, easy way.
I feel like it's easier to digest it this way rather than just building the entire thing and you just building along with me without understanding what we're actually doing. So that was the suggestion state and that was the render plugin. How about we make it even better by adding the accept suggestion keymap plugin. And that's pretty easy actually. So that is tab to accept.
Because right now we can see the suggestion but there's no way of accepting it. So I'm going to go ahead right here and I'm going to build it. So const acceptSuggestionsKeyMap is keyMapOf. Go ahead and find the tab key, run, view. Let's go ahead and find this suggestion using view state field and suggestion state from above.
If there is no suggestion let me go ahead and copy it. In that case we simply return false. Basically that means no suggestion found let tab do its normal thing. Which in our case will be to indent. Otherwise if suggestion was found, let's go ahead and find the cursor position and then let's go ahead and let's call view.
Dispatch and inside of view.dispatch first things first, insert the suggestion text. So changes from cursor insert suggestion, insert the suggestion text then let's move the cursor to the end of the suggestion length. So we have to move our cursor now after we've accepted that new thing. And finally, we have to clear the suggestion. So we simply call setSuggestionEffect of null.
And last thing, return true, which means we handled tab, don't indent. We are kind of overriding the native tab functionality. Well, not exactly native, but... Which is the one? Keymap, indent with tab, right?
We are overriding this. So now, if we've done this correctly, when you hit tab, you can see this is actually inserted now. Try refreshing. After that, suggestion is lost. So you need to refresh.
So try going somewhere like this and press tab and you can see It's inserted now. So we are successfully accepting a suggestion. The problem is suggestion right now is quite dumb, right? It doesn't do anything. It has no idea where we are.
It has no context. It doesn't do any AI stuff. But this is kind of the simplest extension I could think of to help you build this. So now it's time to go to stage two. In stage two, we start differently.
So let's go ahead. Where am I? All the way to the suggestion state. In stage 2, instead of returning a premade string, we are going to start with null. So we start with null because we aren't going to have anything in the beginning.
And then we have to create something called debouncing and suggestion generation. So I'm just trying to find a proper place to do this. So I'm gonna do it after the suggestion widget here. So stage 2 will basically be creating the debounce functionality. Start with debounce timer which can be a number or null by default null.
IsWaitingForSuggestion will be false and debounceDelay will be 300. Now what we have to do is we have to build a fake suggestion. So still no AI calls yet, let's just do generate fakeSuggestion. Text before cursor will be one thing we're going to accept. So now we're building a kind of more advanced text suggestions simply because we will have some information, for example, like text before cursor.
And we're going to trim that. So whatever we have passed here, trim the end and then let's just check for example if trimmed ends with constant return my variable. So if I start typing const, ghost text will suggest my variable. So somewhat smarter, we're just learning how it works. And you can add as many examples here.
It doesn't really matter. You don't have to add all of these. I'm just adding them so it's easier to understand the state of our current app. And last, return null here. So we are mocking suggestions, Raoul.
We are making them up. We are kind of pretending how AI is going to behave. Now down here let's go ahead and let's create the debounce plugin. So let's create the debounce plugin which accepts file name and return view plugin dot from class. And let's go ahead and define the class here again.
So we're going to have a constructor, a view type of editor view calling this dot trigger suggestion with the view. Then besides the constructor we're going to again have an update which accepts update which is a type of a view update. If document has changed or selection has set, call this.triggerSuggestion and pass along update.view. And then finally, let's go ahead and let's implement the trigger suggestion here. So this accepts the editor view and first things first, if an existing debounce timer exists, let's clear it.
So this is our debounce protection. Now let's go ahead and change the variable isWaitingFromSuggestion to be true. And now in here I'm going to define my debounce timer to be window setTimeout asynchronous method and in here let's go ahead and do a fake suggestion and I'm going to just add a little parenthesis here Delete this block later in stage 3. So this is now stage 2, right? So let's go ahead and start by seeing where is our cursor.
Then let's go ahead and do what line we are currently at and let's find text before cursor line text slice starting from zero and going to cursor minus line dot from. And then we are going to generate a fake suggestion using this text before cursor. So this will return something. Right? It will either return my variable, my function, log or null.
We are kind of learning what AI will be doing in this example. So right now this is just, well, fake. Let's now change isWaitingForSuggestion back to false. Let's call viewDispatchEffects.setSuggestionEffect.offSuggestion. Like so.
Dispatch effects set suggestion effect dot off suggestion like so and in here add a debounce delay which we have defined above Looks like we are still not using this is waiting for suggestion anywhere but we will later for now let's go ahead and just finish this. So after the bones dot delay and after this function here let me see, I'm trying to figure out where am I so this is a class let's just see I think we should do it here, destroy let me see Is that working here? If debounce timer is not null, clear timeout, debounce timer. All right, I'm trying to figure out if I did this in the correct indentation. So this ends the trigger suggestion and after that we call destroy.
I think this should work just fine. Now that we have this create debounce plugin Let's go inside of the render plugin right here. Update is fine as is, but build should be modified now. So what we're going to do here is we're going to call if isWaitingForSuggestion return decoration.none. Is waiting for suggestion return decoration.none.
So if the suggestion is currently in process of being created, do not return anything. So that's one thing we have to do. And then let's go down here to our export cons suggestion. And it should be a little bit different now. So we should still have the suggestion state.
But we should also now have create debounce plugin and pass in the file name like this. So what that's gonna do is it will trigger suggestions on typing. So if we've done this correctly, I'm just going to do a quick check through the code. If we've done this correctly, we now should not see the default suggestion only when we start typing something. More specifically, only when we start typing...
And yes, file name is still unused, that's fine. So let me find where is the generateSuggestion function. I'm trying to find it. So if you type const, you should get my variable back or if you type function or console or return. Let's try all of them here.
So I'm going to go ahead at the bottom of the file. I can see that No suggestion is happening on cursor change. Good. If I type gibberish, nothing is happening. But if I type const, there we go.
I get my variable. If I get function, I get my function. If I add return, I get null. If I write console, Does it work? Is it console?
Oh, it's console dot. Okay. Console dot. I can see out a suggestion for log. So yes, Obviously, it's working.
Perfect. Now the biggest problem is it's still not calling any AI. This is all still just fake. So what we have to do now is we have to start building the actual endpoint for this because the AI suggestion cannot work without calling an endpoint. So in order to build that let's go inside of source, app folder, API and in here I'm going to build suggestion and inside let's add route.ts.
Let's start by preparing some imports, such as generate text and output from AI. Let's go ahead and import next response from next server. Let's go ahead and import z from Zod. And then import your provider. For example, Anthropic, not from Ingest, from AISDK Anthropic or Google from AISDK Google.
So whichever one you chose to use. Now we have to build the suggestion schema which is going to be a Zod object which accepts a single property called Suggestion. It's a type of string and we describe it as the code to insert at cursor or empty string if no completion is needed. Now what we have to do is we have to build the suggestion prompt. Suggestion prompts are always kind of tricky and it definitely makes no sense to type it out here.
So you can just go ahead and visit the Polaris Assets folder using the link on the screen and in here I have prepared first of all you can find both my suggestion extension here and the fetcher so if you don't want to go through me going through these stages of the suggestion You can just find the full code here. But if you are going along with me, you need the prompt. So here it is, the suggestion prompt. It's not really any magical, I just kind of built it and changed it until it works. You can see later we also have another prompt here, but for now this is the one we want.
So let's just add suggestion prompt. You are a code suggestion assistant. And What I'm using here is XML type of syntax because that works very well with Anthropic. I have no idea how well it works with Google. In fact, I don't know what works well with Google providers.
If you know, feel free to change this and alter it. Maybe ask Gemini to kind of modify this prompt for Gemini. But for Anthropic, this works great. It's basically instructing it what it is. Feel free to use this prompt for Gemini.
I think it should work just fine. Now let's export asynchronous function post. We get a request. Let's go ahead and open try and let's also prepare a catch here. So in here I've started extracting some things and that's the following.
File name, code, current line, previous line, previous lines, text before cursor, text after cursor, next lines and line number all from awaitRequest.json. And if we don't have the code, which is the most important part of this request, we're simply going to go ahead and throw a nextResponse.json with an error code is required and pass along a status of 400 meaning whatever came from the front end is not good. We cannot work with that. Now what we have to do is we have to create a prompt from the suggestion prompt by replacing file name to be the actual file name then we have to replace code to be actual code and then the exact same thing for all other things Current line for current line, previous lines for previous lines, or fall back to an empty string. Text before cursor, text after cursor, next lines or fall back to this, and line to number, make sure you transform it to string.
So this is the prompt. Do you need all of these? Probably not. You can maybe just do, I don't know, text before cursor. The more context you give it, the better it's going to perform, obviously, right?
But you can do fun things with just code and file name, right? But I think at least current line and text before cursor, I'd say like these ones are the most important ones. So otherwise, how does it know what to even suggest you, right? And now that we have our prompt we can go ahead and create this output using await generate text model. Now in here it's going to be Anthropic or it's going to be Google.
And if it's going to be Google, you obviously have to give it whatever model you're using. For example, Gemini 2.0 Flash. Output.object schema, suggestion schema and prompt. And let's return next response.json. Suggestion.
So, this is important, right? What you respond is important because that's what we're going to look at at the frontend. And simply pass in, my apologies, output.suggestion. So you can see this is hardcoded dot suggestion. I mean this is typed dot suggestion.
Why? Because we gave it, where is it? We gave it an output dot object, right? So right here we have the suggestion schema which accepts a suggestion. So when we pass that here, suggestion schema, it knows that's what it has to return.
So I'm not sure if this is the proper Google model to use. I will try with Google first because I know a lot of you are using the free API key but then later I will switch to Anthropic simply because the results are much better but I think we should get some fun results with Google one as well. And let's also just catch the error, right? If we do have an error, let's console.error, suggestion error. And let's pass the error and let's return next response.json with An error, failed to generate suggestion.
This will also get logged to Sentry so no worries. Let's go ahead and end it like this. There we go. So that is our endpoint. We can comment out and drop it now.
So that is our endpoint for suggestions. What we have to do now is we have to create a Fetcher. So we can do this in a very very simple way but I just like some level of type safety when it comes to you know calling these things. So to make it easier and less error prone I'm going to go inside of features, editor extensions, suggestion and in here I'm going to create Fetcher.ts. And in here let's go ahead and install a package called ky.
So ky is like a lightweight alternative to fetch and Axios. Maybe specifically Axios. It's not an alternative to fetch. It probably definitely uses fetch inside. But yes, like a lightweight alternative to Axios.
So let's go ahead and install, import ky from ky and Let's import Z from Zod. And now what we're going to do is we're going to create the suggestion request schema using Zod. So what is this? Well remember how we had route.ts? In here we had all of this.
So now we have to validate all of them here in this object. So let's go ahead and create that. File name is a string, code is a string, current line is a string, previous lines, text before cursor, text after cursor, next lines and line number which is a type of number. And we also have to define what do we get back. Well a very simple suggestion which is a type of string.
So let's define that. Suggestion response schema is an object with a string back. Now we can go ahead and infer from those Zod definitions to create actual types. So z.infer type of suggestion request schema and suggestion response schema and now we have nice types which we can use here. Now let's export const fetcher.
It's an asynchronous method which accepts a payload which is a suggestion request and a signal which offers the option to abort signal. We can use that in case the user starts typing again so we can easily abort the request we just started to create and thus we can save some tokens so we don't create unnecessary AI things. Let's open a try and catch here and the first thing we're going to do is we're going to validate the payload. So, validated payload is SuggestionRequestSchema.parsePayload. So if whatever we pass to this Fetcher function doesn't pass this, we're simply going to throw an error.
And now let's go ahead and get the response by doing await ky.post forward slash api forward slash suggestion json validated payload, pass in the signal timeout is 10, 000 and retry will be 0 and let's go ahead and chain .json and execute it and give JSON a type of suggestion response so we know exactly what we're getting back and then let's do const validated response suggestion response schema.parseResponse so we do another check Did we get back what we expect to get back? And then finally, return validated response dot suggestion like so. Or if we don't, we can get null, but I think we should always do it you can leave it like this it should be fine so in the catch the only thing that's important is to differentiate if the error is because we cancelled it with the abort signal so if we did so if error is an instance of error and error name is abort error, don't do anything, just return null. Otherwise, return null as well. Great.
So now while I'm here, I also want to import toast from Sonar. We already have Sonar installed. It comes with chatCNUI and you can see 2.0.7. And I just want to throw an error here, toast.error failed to fetch AI completion. Simply so the user is aware this is not working.
But in order for the Toast to work we have to quickly revisit not the environment file, the app file layout file right here and let's just add toaster from components UI sonar like this and let's just move it here that's it that's all we have to do great so that is the fetcher But now what we have to do is we have to implement stage 3. So let's go ahead inside of features, editor, extensions, suggestion, index.ts. So What should we do first? Well, we should import our fetcher. So let's go ahead and go at the top here and import fetcher from .slash fetcher right here.
Once we have the fetcher, we should start doing some changes. So we no longer need the generate fake suggestion. We can get rid of that but let me just add to do clean up this fake function because it is fake right it's not doing anything it's pretending it's AI and now we are going to have a proper AI method So let's start by doing const generate payload. And that will accept a view, editor view, and a file name, which is a string. And now let's go ahead and start by getting the entire code.
So view state document to string. That's the entire code. There's a chance the code is completely empty. So if there is no code or when we trim the code the length is zero, return null. No reason to make an API request over something that small.
Now let's go ahead and define the cursor position using view state selection main head. Then let's go ahead and let's get the current line using view state document line at cursor position. Then let's go ahead and get cursor in the line, which means cursor position minus the current line dot from. Now let's go ahead and get the last five previous lines. So prepare an array like this.
So we're going to get five of them, previous lines to fetch, math, minimum five, and then a current line number minus one. And then in here, we're going to do a simple for loop to achieve that. For, Let I be a previous lines to fetch, I is greater or equal than 1 and simply go down to 0, I mean to 1. And previous lines and then for each line that you find, go ahead and push it to the previous lines array so view state doc line and we find it using current number minus the current iterator i.text a bit complicated so pause and make sure you write it correctly And then we do a very very similar thing for next lines, right? So first let's find the total lines using view state document lines.
Then let's go ahead and define which lines we should fetch next. So math min 5 total lines minus the current line dot number and then again we do a for loop here with the iterator starting from 1 being less or equal than lines to fetch and increasing. So then we do kind of the opposite thing. In here we went up and in here we go down. But the logic I can see highlights it's exactly the same.
Except this is plus the iterator and this is going minus the iterator. And the logic is in reverse. This is I minus minus and this is I plus plus. So just be mindful of that. You can always pause to double check.
Or if you're just unsure, you can always use the assets here to just see the complete suggestion extension. How it looks in its final form. I will share that with you again later. And you also have the fetcher. Just in case, since I know this is a bit complicated and it's an important feature I want you to have it working so I'm going to make sure that you have the answer.
All right. And now what we have to do is we have to return all of those things. So things like file name, code is simple, but then we get to a bit more complicated things. For example, current line will be current line dot text. Previous lines is an array.
So we are using previous lines dot join with a page break like this. So we are preparing this for AI consumption. TextBeforeCursor is currentLine.text.slice starting from 0 and going to cursor in line. Text after cursor is currentLine.text slice cursor in line. NextLine is an array again so we join it using page break.
And finally we have the line number which is currentLine.number Great, so now let's go ahead inside of createDbouncePlugin and now after we do triggerSuggestion here after we do this let's go ahead and do an abort controller so let me just see ok we have to go right here let's go above generate payload actually I'm trying to find the best place to do this. I want to do it here with the debounce timer. So just here go ahead and add let current abort controller to be a type of abort controller or null and default it to null like so. And now let's go ahead. Let me see abort controller exists.
Let's go ahead and use it down here in the create debounce plugin so after trigger suggestion I started to leave some space here So if current abort controller is not null, call current abort controller dot abort. It seems like I'm having some problem here. Abort does not exist on type never. So let me quickly check what that's about. I think this is perfectly fine.
It's because we never actually give it a type of abort controller so it's confused about what it has to abort. And we do that here. So this is a fake suggestion. So we can delete this block and we are now in stage 3. So okay, let's delete this entirely and just leave this.
And instead we're going to add payload using generate payload, the method we defined above which accepts the view and the file name which we pass in here. So we have access to view from Trigger Suggestion and file name from the Created Debounce plugin here. Awesome. So that's the first thing. And now we have to check if there is no payload.
We have to switch isWaitingForSuggestion back to false. And we have to update our view dispatch and set suggestion effect of null. And then let's go ahead and do an early return. And then outside of this if clause, assign to the current abort controller to be new abort controller and then finally the new suggestion will come from await fetcher payload and pass in the current abort controller dot signal here. So basically it's going to accept two things the payload which we generate and the current abort dot signal and in here is waiting for suggestion is again false The view dispatch is exactly the same.
Very good. Okay. Now what's missing is in the destroy method here. So we should also, besides doing the debounce check, we also have to check if current abort controller is not null. Let's make sure to abort it so we don't have any memory leaks.
Great, so now I am just checking, you know, is there anything important I've missed here and I think this might be it. Let me see, what is the warning about here? Generate fake suggestion, yeah. We no longer need it. We can remove generate fake suggestion, we can keep all of these close together.
And let's Check it out. So again, I have no idea if it's going to work the first try simply because I've set up to use Gemini and I'm never familiar with their code. So let's try picking something like this, right? Or you can just copy one of your components. So if I just go ahead and add on something, let's see if it will give me any suggestion or it gives me an error.
Okay, I think this is an error because of an invalid model. This happens almost every single time but at least the toaster is working on failed to fetch AI completion. So I'm going to go to aisdk.dev simply because I want to show you how I fix problems like this. I mean this only happens with Gemini for me. So like no one else.
Let me go ahead and try going inside of providers and I'm going to try and find where is Google. Here it is. Google Generative AI. What did they use? Gemini 2.5 Flash.
Okay. I don't know why this is like that. So let's go inside of app folder, API suggestion. Also make sure you, you know, you can see suggestion here and in here API suggestion. Make sure you didn't accidentally misspell the folder name.
It should be inside of API and should have route. So I'm going to change this to be 2.5 flash. Maybe we'll have more luck then. Let me refresh again. Let me go inside of this one.
Let me write on change to and will it auto suggest something or not? There we go. It works. If I start writing const, let's see, will it think of something? Sometimes it can decide that nothing should be added here.
Sometimes it just fails. Let me go ahead and see why it has failed. So the problem is I never know, you can see it's telling me I have exceeded my current quota, but I'm almost 99% sure that's not the case. I just think it works funky. Sometimes it works, sometimes it doesn't.
Let me try writing const on click here. Will it work or not? Looks like not, but let's simply check. If I go ahead inside of my route.ts suggestion and if I change this to Anthropic. So I mean for you Google might work perfectly fine.
But for me it's always funky. I don't know why. I mean we are talking about their free tier, right? So that's obviously different. And yeah, for something like this, when it comes to a thropic, you should kind of use one of the cheaper models like Haiku or something like that, simply because this shouldn't be too expensive, right?
I mean, in my original source code, it seems like I have used this 3 7 sonnet. Now I'm not sure what specific version right. But let me go ahead and refresh and see if this will work better. So I will delete this and do it again. Const on click.
Let's see. There we go. You can see how Anthropic works very nice. Const on enter. Let's see.
There we go. And if I move, it resets. That's what's important too. We have to test all of those things and when I come back here I can still accept. Very very cool.
Make sure your indentation is still working fine. Let's see if I go ahead and just add a new line here. Will it maybe suggest some extension here and sometimes it just won't do anything and that won't be an error it just decides I shouldn't do anything let's see, if I add a console here will it suggest maybe a log here it is, cleaning up editor view, a very relevant log, so it completely understands the lines coming before and after our code. So it definitely works, I'm just not sure how do I make it work reliably with Google. I mean you saw for a second that it does work right so obviously something is working here but I just have so little awareness of like it's models that I don't know what's good and what's bad.
Let's try Gemini 2.5 Pro. I don't even know, can I use this in free tier? If I add on key down here, this should be a relatively simple auto-complete, but I don't know. It seems like it's just not working. Like I want to give you the option to use free AI models, but they're just not reliable for me.
I don't know. If maybe for you they are working, I mean, it's telling me I have exceeded my quota. So I think that might also just mean that I'm using a Google model that should not be used for the free tier, but I'm not sure. I have no idea. If you have better knowledge of Google, you might solve this for yourself, right?
Oh, there we go. That works. Let's try const onClick. Let's see. Will it suggest something for me or will it throw an error again?
You can see it does work obviously but I guess it's just the fact that it's free tier is not too powerful So it very easily hits some limits, I guess. But yeah, it definitely works and it works quite well. You can see how it suggested just a single line and nothing more than that. But then very soon it starts to fail, right? So because of that, I'm gonna switch to Anthropic.
Again, I understand a lot of you are not able to do that, you know, but I'm not really sure what to do in this situation. Feel free to try other models. I mean, there's XAI, right? So you can import XAI, just have to install AI SDK, XAI, and I think they offer free tier 2. There's DeepSeq, right?
Feel free to, you know, research because any model can work with this. It's just that the free ones are not reliable when it comes to rate limiting so that's why I'm using Anthropic to demonstrate and prove that our code is actually working, right? It's just the fact that the models that we choose will depend on the results. See? Anthropic works perfectly, Immediately throws something relevant.
Excellent. So amazing, amazing job. So what can be improved here? One thing that immediately comes to mind is this. We are not doing any validation on the server, but We are doing it here in the Fetcher, right?
So let me see if it would be easy to maybe share these suggestion request schemas and then validate them. I mean, I don't know. We are validating them here in the Fetcher, so we know that this JSON is pretty reliable. But it could be a good idea to also verify it here. So it shouldn't be too complicated to add that, you know, but for tutorial purposes, I think this is just perfectly fine.
:03 We do have a validation here. Obviously, if you expect someone else to call this endpoint, that's not your app, well, then you should probably, you know, limit this and validate it and things like that, right? But for a purpose like this where it's just our app, it's fine, right? But obviously, you know, this API suggestion should only be hit if you're logged in. So that's one of the things we can actually do here.
:31 I think we can just do user ID await out from clerk next JS server like this. And then if there is no user ID return next response dot JSON and pass in, let's see, how do I return errors like this, right? Just to make sure that no one, no malicious actors are trying to access this. Okay, let's kind of do it like this. And this would be unauthorized with a 403, like that.
:20 So now only logged in users can do this. And then later when we enable billing, we can also super easily check that the user is also on a pro plan, right? So no one can spend your tokens who isn't paying for your SaaS. So yes, that's one thing I would recommend doing. And since I'm logged in, I would expect being able to do this.
:43 Let's see. There we go. Still works perfectly fine. Awesome. Great.
:51 So, let's see what we have to do next. So I think it makes the most sense to immediately implement the second custom suggestion we're going to have, which is the suggestion tooltip. So let's go ahead and start by going with the API route because this time we won't be going through stages, right? We won't be doing any mock things. We're just going to go ahead outright and build it.
:20 So quick-edit and inside route.ts. So this time I'm going to be using Anthropic simply because I'm having problems with Google, but again you can go ahead and import Google and use Google if you prefer. Besides that we're also going to import Firecrawl using at lib Firecrawl. Let's go ahead and define the quick edit schema which is going to be a Zod object and it accepts edited code. The edited version of the selected code based on the instruction.
:52 So this is a bit different because the user will directly select and highlight a line of code it wants to modify. Let's define the URL regex. You can Google this if you don't know which one it is. Or you can pause the screen. And now we need to define the quick edit prompt.
:12 So you can head to my Polaris suggestions. My apologies, Polaris assets. And you can find prompts.ts. And in here we have suggestion prompt and here we have quick edit prompt. So let me go ahead and copy it.
:28 Now let's go ahead And let's paste it. So here it is. Quick edit prompt. You are a code editing assistant. Edit the selected code based on the user's instruction.
:40 Return only the edited version of the selected code. Maintain the same indentation level as original. Not include any explanations or comments unless requested. If the instruction is unclear or can't be applied, return the original code unchanged. All right.
:55 And now let's export asynchronous function post, accept a request, and let's open a try and catch. So what we're gonna destructure here from the request.json is simpler. Selected code, the full code for the context and the instruction. And let's check if there is no selected code, simply throw selected code is required. If there is no instruction, Instruction is required.
:32 And while we are here, we can also do user ID from await out from clerk next JS server. Let me go ahead and just move this here. I like to order them by length. And now in here I'm also going to do the exact same check. But just if there is no user ID unauthorized.
:02 All right. So now that we have checks for all of those things, let's see if the user gave us any URLs. URLs is going to be an array of strings and we're simply going to go over the instruction the user gave us and match it using the URL regex which we've defined up here. Otherwise fall back to an empty array. And then let's go ahead and define the documentation context.
:28 So if the URL's length is larger than 0 let's go ahead and scrape the results. So const scrapedResults await promise all URLs.map asynchronous get the individual URL open a try and catch block. And in here attempt to get the result using await firecrawl scrape URL in the Markdown format. If result.markdown return and then the syntax I'm going to return here is very specific, again works very well for Claude, for Anthropic. It's in format of XML.
:17 If you want to, you can literally just return result.markdown. But this will kind of make it serve better as context when it comes to... When it comes to... I'm not sure what I'm trying to explain, understanding that this is an additional documentation, something that was scraped, right? This exact format, like a XML syntax, this is a document, this is the URL and this is the result of the URL.
:48 But again, you can just return result.markdown. Otherwise we are returning null and in the catch we are returning null as well. And then let's go ahead and only grab the valid results from that. So let's see. Scrape the results.
:09 Scrape the results. Filter Boolean. They now turn into valid results. And then we're going to push all of these if they are more than zero, we're going to add documentation context to be the following. Inside of the documentation XML tag, we're gonna add a page break, and then we're going to join an array of these using more page breaks.
:35 So basically it's going to be like documentation and documentation and in here URL nextjs.org proxy.ts right and then in here blah blah blah what is proxy and doc And just a bunch of doc URLs. That's how the result is going to look like. So again, you don't have to use XML, but it works very well with Clause. So if we have any valid results, we simply append the documentation context with that XML tag and we join all the valid results using double page break here and we end with another page break in here to format it nice for the AI. Awesome.
:20 So now outside of this, if URLs length is larger than zero, let's go ahead and modify our prompt. So that's going to be quick edit prompt. And let's start replacing some things. So the first thing we're going to replace is selected code with the variable selected code. Then full code or an empty string.
:46 Then instruction with instruction. And finally documentation with the documentation context. And then let's go ahead and define the output using a way to generate text. Again I'm using Anthropic and Claude model. You can use Google here.
:06 Just make sure to use proper Google model. You can use XAI, whatever you want. I'm using Anthropic because it's very reliable for me. And now I'm just going to go ahead and return next response.json edited code output.editedcode and you can see that is the edited version of the selected code based on the instruction which we have defined in the quick edit schema. And in the cache here, we're just going to go ahead and throw some errors.
:34 So make sure to grab an error here and go ahead and just throw like this. Edit error and next response that Jason failed to generate edit. Awesome. So that is forward slash quick edit. Now what I would like to do is I would like to go inside of projects, my apologies, inside of features, editor extensions, I'm going to copy and paste suggestion and I'm going to rename this to quick-edit.
:06 And I'm going to go inside of Fetcher here and just modify it so it works for Fetcher. So it's not going to be called suggestion request schema, it's going to be called edit request schema with selected code, full code and instruction. And for the edit response schema, we're just gonna have edited code Z.string. That's also gonna change these two types. It's gonna be edit request type of edit request schema and edit response type of edit response schema.
:37 So let's see. This will now be edit request. And this will now be edit request schema like that. The API endpoint will go to quick dash edit. We can increase the timeout to 30, 000 because we allow this one to think longer and let's use edit response here and let's use edit response schema and this is edited code or now and this will be failed to fetch AI quick edit.
:15 There we go. So that's a very quick fetcher modified. And now we go to the extension of the quick edit. So the extension of the quick edit is a bit different. Let's go all the way down here.
:31 I personally think it's a bit simpler so don't worry and we won't go through all the stages we're just going to build it out right. So export const quick edit. We're actually not going to be using the file name but maybe that's not such a good idea. Maybe we should use the file name. For now I'm just going to leave it here as a prop but I'm not going to use it anywhere.
:50 So the first thing we're going to do, we can remove all this, is going to be Quick Edit State. So I'm just going to remove everything up to the state. That's the only thing I'm going to leave. There we go. We obviously are going to still use the fetcher and I'm just going to remove all the comments simply because I think I removed something other than a comment.
:14 Right. Because we explained how extensions work in the previous one and I think now we can just focus on building one. So quick edit state. Let's go ahead and start with setting the effect. So it's not going to be set suggestion effect.
:30 It's going to be show quick edit effect state effect define boolean. Then let's go ahead and let's define a few things here. Editor view to be a type of editor view or now and the current abort controller to be abort controller or now right here. I'm going to make it easier and just delete everything like this. So let's go ahead and define the quick edit state.
:57 The quick edit state will be a state field dot define which accepts a boolean and we're going to start with a simple create method in which we are going to return false. We're then going to create an update method which has a value and transaction and let's go ahead and search for const effect of transaction.effects if effect is show quick edit effect, return effect.value and then after that for loop if transaction.selection exists Let's go ahead and get the selection. And if the selection is empty return false. So if the user didn't select anything return false. Otherwise just return the value.
:44 Great. So now our quick edit state is finished. So now what I want to do is I want to add quick edit tooltip field. Let's go ahead and build this one next. I'm just trying to see...
:03 Okay, yes. So in order to build that, we have to define the following function. Create quick edit tooltip, which accepts a state of editor state and returns read only tooltip. I mean an array of tooltips. And let's start with getting our selection.
:25 If the selection is empty, return an empty array. Then let's go ahead and see is quick edit active, right? So that's why we are returning false here because by default it's not active. That's how this is going to serve. And if it is not, again return an empty array otherwise return an array and an object inside and now in here the position is select.to above is gonna be false strict side is gonna be false and then we're gonna call the create method The create method will create a div element.
:13 We're then going to give that a class name. So we're just styling it now. So this class name right here that you're seeing. Let me show you how that's going to look like. So it's easier.
:23 So if I switch to localhost three thousand and five here this is my finished product here. So when I select something and click quick edit, this is what we're building. So this is cancel, submit, this is the div, the input, right? We currently do not have that. If I select something, nothing happens.
:46 And if I press command K, nothing happens. So that's what we're doing now. Now after that DOM class name we need to define a form element and we need to give that form some class names flex, flex call and gap2. Then we're creating our input element where the user will actually type what they want. We are going to give it the type of text placeholder of edit selected code.
:12 Let's go ahead and give it a class name of background transparent border noun none outline none padding on x axis to on y axis one font sense and a width of 100. Then autofocus on true. Then let's create a button container which is going to hold our two buttons. So another div with flex item center justified between gap 2. Fun fact, this is how we build components before React.
:40 So now let's go ahead and do a cancel button here and let's go ahead and give it some properties. Type is button, text content is cancel. Now let's go ahead and give it some class name, font sense, padding one, px2, text muted foreground, hover text foreground, on hover bg foreground with a 10% opacity, and rounded small. Now let's go ahead and define what happens when we click the cancel button. So when we click the cancel button if we have the abort controller let's make sure to abort and return back to null.
:14 Otherwise, I mean still if we have editor view let's call editor view.dispatch effects show quick edits off and set it back to false. So now let's go ahead and do very similar to submit method. I mean submit button. Submit button is another button. Submit button type is submit.
:37 Submit button text content is submit. Let's go ahead and add a class name font sans padding 1 px2 text muted foreground basically I think identical to this one. I don't think there are any differences. And this one won't have its own on click because this is a type of submit. So instead we are going to access our form element on submit.
:00 And the first thing we're going to do is make sure to prevent default so it doesn't refresh the page. In case we cannot access the editor view, we're going to break the method. Then we're going to attempt to trim the instruction and if it is not available after the trim, it means it's empty so we return it. Then we're going to go ahead and get the selection from the editor view state selection name. We are going to get the selected code using again editor view state document slice string selection from selection to.
:31 These errors are fine. We're going to fix them later. Then let's add the full code, editor view, state document to string. Then let's go ahead and make sure that while this is submitting, we set the submit button to disabled and change the content to editing. Let's go ahead and assign the new abort controller here.
:52 And finally, we can go ahead and get edited code using our fetcher. In the first argument we send the payload, which is selected code, full code and instruction and in the second argument we send the abort controller signal so that we can abort if user changes its mind. So now let's go ahead and open if the edited code was received, we now have to call editor view dispatch and we have to modify it. So we first have to define where. Changes from selection from to selection to insert what the edited code we just got back from the API.
:35 Then let's go ahead and make sure to modify the selection. So we move it to the end of the edited code and return this state to false. So it's no longer opened like that. Else, if edited code was not received, we're simply going to make sure that we reset submit button disabled back to false and this back to submit. Which most likely means something went wrong.
:59 And finally, current abort controller is null. And now what we have to do is we have to append these things to button container. So cancel button and submit button. And then we have to append all of those things to form. So append the input and append the button container.
:17 And finally, append the form to the DOM. And to make autofocus work, let's do a simple setTimeout trick. And then in the end, let's return DOM, like this. Quite a long function, but a very useful one. I mean most of it was just building the UI, right?
:37 This would be way easier to write if we had access to JSX and React but we are in this environment where we have to write code this way. But yeah, this is how people wrote before React actually. All right, so we have that. Now we can define quick edit tooltip field. So const quick edit tooltip field, state field, define, read only, Tooltip and then an array Like this Let's go ahead And add create here And let me just see Okay, the tooltip needs to be imported.
:18 Yeah, let me fix all the imports actually. So from CodeMirrorView we're gonna need Tooltip, ShowTooltip, Keymap and EditorView. And from State we're gonna need StateField, EditorState state effect and then the fetcher. Okay. Let's go down here where we started building the quick edit tooltip field.
:49 So we have create here. Now let's create an update which accepts tooltips and transaction. And in here, if transaction document changed or transaction selection, return create new quick edit tooltip basically we need to define where to render the tooltip to render to allow user to write something we can't just render it anywhere we need to be aware, is the cursor here, is the selection here, right? That's what this update is doing, It's kind of like doing the visual thing. And then outside of this if clause, do a for loop.
:30 So again, searching for an effect in all of our transaction effects. If we find one with show quick edit effect we return again create quick edit tooltip with the current transaction state. And let's finally go ahead and return tooltips. And then in here let's add provide field and return show tooltip, compute n. First argument is an array of field, Second argument is a function which accepts state and returns state dot field and passes in the field.
:09 Like that. Great. And now there are a couple of more we have to implement. So they're just easier than all of these. So the next one is quick edit key map.
:21 This will basically allow us to use a shortcut to open this. So quick edit key map here open an array insert an object here The key will be mod and then the letter K and what that's gonna do is it's gonna run something with the view so we get the current selection where it happened. If the selection was empty there is nothing we can do but if it isn't we call view.dispatch effects show quick edit effects off and pass true and then we return true simply to override if mod K was any other method already. Great! So that was a simple one And the last one we need is capture.
:08 The last one we need is capture view extension. This one is the simplest capture view extension editor view update listener of update editor view update you. I think this just refreshes the state like when we type in things. I will double check. I'm not 100% sure.
:28 It is my first time building code mineral extensions too. So I'm trying my best with that first one to go through stages so we both understand how they work from primitives. But with this one I think we can just go ahead and build it. Okay, I think, yeah, and you can see all errors actually went away as we built. So all of this should be fine.
:49 File name is expected to be like that here. So now let's go inside of our components code editor here. And after suggestion pass the quick edit from extensions quick edit and simply pass the file name here too even though we don't use that. So now what you should be able to do is select the thing like this using control key or command key should open this edit selected code and I should be able to say rename this to folder name and I should be able to press enter and submit and It was just successfully renamed to folder name. So if yours is not working, make sure to try Ctrl key, Command key, you know, whatever your action key is, whatever your modulus key is, mod key is.
:45 You can also like manually change this if you want to just try it out. You can try, I don't know, the letter B. I think this might work. So if I highlight this and do Shift B, it opens. Shift B, try that.
:06 Or if I use a lowercase letter B, I think it's then going to like pop up every time I press letter B. Yes. So every time I press the letter B, it opens. So let's bring it back to mod K. And now to wrap this up there is actually one more thing we have to do.
:25 I mean you might be satisfied with this as it is but it might be like cool to have, let me go ahead and show you, it might be cool to have an ability that when you highlight in general, it shows you the option to quick edit. So let's go ahead and implement that. This one actually doesn't include any complicated logic. It's just tedious to write the components that way. But let's go ahead and do it.
:54 So one thing I already know we're going to have to do is inside of the extensions inside of quick edit here index we're going to have to export I think this one I think we're going to have to export quick edit state and also yeah so export show quick edit effect and export quick state. Make sure you export both of these two. And now we're going to go ahead inside of extensions and we're going to create a new one and this one actually no need for a folder. This one is simpler So we can just call it selection-tooltip.ts. So selection tooltip.
:36 And let's go ahead and start with export const selection tooltip, which will accept selection tooltip field and capture view extension. We can go ahead and immediately fix the capture view extension. It's editor view update listener of update editor view update view. The exact same one we had from before. I mean in this one.
:00 If you scroll down here you will see at least a thing. It's exactly the same. So for the imports of selection tooltip let's go ahead and import tooltip, show tooltip and editor view from code mirror view, state field and editor state from code mirror state and show quick edit and quick edit state from quick edit. And now let's go ahead and start developing this, right? So I'm going to define editor view to be editor view or null and then I'm going to go ahead and create a function, create tooltip for selection.
:35 It's going to accept state and it's going to return a read-only array of tooltips. We're going to start with a selection and if selection is empty we're going to return an empty array then let's check if quick edit is active like this if it is active we're gonna return an empty array otherwise we're gonna go ahead and build UI so let's go ahead and again start with the position, which is selection two, above false, strict side false. And then we go ahead and build the create. So again, we start with the DOM, right? We then go ahead and give the DOM class name, bgPopover, textPopover, foreground, z50, rounded, small, border, border, input, padding, 1, Shadow, Medium, Flex, Item, Center, Gap 2 and Text Small.
:37 We then go ahead and start with Add to Chat button which we will see how we will implement this functionality later. But for now it's just going to be visual. So let me just properly indent this. There we go. So add to chat button is a button with text content added to chat and class name here.
:59 And Then we have the familiar one. Quick edit button. Again, document, create element button. Let's go ahead and give it the following class name. Feel free to pause the screen to copy the class names.
:11 Now let's go ahead and give the quick edit button the span element. Quick edit. So text content is quick edit. Let's go ahead and create a shortcut element. So quick edit button shortcut is another span element.
:31 The text content is again this is my Mac OS control sign right so command K text small opacity 60 you can also write control plus K here whatever you want. And now let's go ahead and let's append these elements. Quick edit button, append child, quick edit button text and append child, quick edit button shortcut. And now on quick edit button on click, if we have the edit review, simply dispatch and change the state of show quick edit effect to be true and that will then trigger this. What we've just built previously.
:13 We did this entire thing before, right? Alright. And then we just have to append all of these to the DOM add to chat button and quick edit button and finally return Dom. Great. So What's left is the selection tool tip field.
:34 So let's go ahead and develop that here. Selection tool tip field, state field, define read only tool tip. Let's go ahead and call create, in which we are going to return tooltip for selection and pass the state along. As always we're going to have an update function which accepts the tooltip and the transaction. If transactions document has changed and transaction selection return again create tool before selection.
:03 You can see this is a pattern right. We keep doing this right here right. So we are okay this is the state not that one. Let me scroll down here. There we go.
:14 See Every time kind of the document changes we have to recalculate where we are going to build the UI. Outside of the if clause here, let's do a for loop. For const effect of transaction effects. If effect is show quick edit effect, again go ahead and build it. And then right here, return tooltips.
:38 And then let's add provide again, which is a field show tooltip, compute and field in array as the first argument, state, state, field, field as the second argument. There we go. And seems like no errors here, So I believe this should work just fine. So now if you go inside of your components, code editor right here, and if you just add selection tooltip from Extensions selection tooltip and let's execute that It should work. So now when I highlight Let me just confirm I am on my Real-time project.
:26 Okay. It's localhost 3000 We are getting so close to the finished project that I don't even know which one is the one. And there we go. Add to chat does nothing, but quick edit triggers this. Rename props to globe props, for example.
:40 Let's see. There we go. Amazing, amazing job. We implemented so many amazing features. We have shadow text, we have a mini-map.
:53 Amazing. So that is 13 files and now it's time to get CodeRabbit to review all of this, right? So I'm just gonna go ahead and shut down all of my terminals here. Let's see chapter 11. So git add dot git commit 11 AI features.
:18 Git checkout dash B 11 AI features, git push uorigin 11 AI features. And then let's go ahead and open A pull request. A lot of changes. I'm very interested in the amount of files we have created this time. I mean the lines of code, but less than the previous pull request.
:40 But still, let's go ahead and review it. So what we've added in this chapter are AI-powered quick edit functionality with keyboard shortcuts, intelligent code suggestions displayed as ghost text while typing, selection action tooltip for selected code, and we did some improvements. We enabled text selection throughout the editor by removing select none from the body. We added toast notifications for user feedback. And we do have quite some comments here.
:08 So 10 comments. Some of them we already are aware of, like the missing Zod schema before the structuring, which I told you you can do of course, but you know, for tutorial purposes I'm going to keep it this way. But yes, obviously a good comment by CodeRabbit. In here I forgot to change to 401 or 403. It definitely shouldn't be status code 400 if I'm throwing unauthorized.
:31 But still, not anything that will break the app, right? In here, we're not using Firecrows timeout feature, so yes, if a single URL hangs indefinitely, it blocks the entire promise all, causing the request to timeout at the client's 30 second limit resulting in a poor user experience. So we could definitely add a timeout to each of the requests here to improve that. Then in Here we have a very interesting comment. This is quite fragile simply because if the user literally types in things like this inside of their instruction, we are going to replace it.
:19 So yeah, it's kind of brittle, but I mean for most use cases I've tried it works correctly but still important for you to know if any input field like file name code or current line contains a placeholder like this one, which we have defined, the replacement will incorrectly modify the user's actual content, corrupting the prompt. Though I'm not sure, since we are directly modifying the constant here, so I'm not sure how this can modify our, Oh, maybe if it is inside of here somewhere. Not sure. Okay. But as I said, for most use cases, I think this is fine.
:56 And now this is interesting. Yeah. So in couple of places, it's telling us about this module level mutable state, which can cause issues with multiple editor instances. So right now, not an issue because we don't have multiple editor instances. But if you in the future plan to implement this, you could probably use a different architecture to not use these global definitions of EditorView and abort controllers.
:21 So in here it recommends using a WeakMap keyed by the EditorView, storing state in a custom state field using EditorView facets. So that's one of the solutions here. In here, actually, this is a new Tailwind utility in version four. No documentation is present, but the class name works. So let's make sure CodeRabbit learns that.
:52 In here it noticed the unused file name parameter, which we are aware of. We will see if we will do something. Non-functional add to chat button, same thing. Same comment as before, so global state shared across multiple editor instances, right? So it's basically telling us that this can cause race conditions if multiple editors are ever a feature, incorrect cancellation, wrong UI state, right?
:17 Basically a bunch of problems so we need to make sure that each editor instance has its own isolated state I will see if this is something that we can easily fix maybe in the next chapter or so you can see it's using, yeah like scoping it to this instead of just using yeah interesting and definitely a good comment here but as I said for our use case this is okay and it kind of says that down here if you read it. So the current situation is OK, but if multiple editor instances exist simultaneously, it will cause a problem. While the current UI architecture only renders a single editor at a time, the implementation is fragile. So if a split view or multi-editor feature is added in the future, these globals would cause race conditions and unexpected behavior. So I didn't really build this with multi-editor feature or split view in mind, but it's good that you are aware of this.
:14 So this is why it's always good to have someone review the code. I will see how easy it is to fix this. I mean, judging by what Dave recommended me here, it's not too difficult, really. Looks like there are some built-in mechanisms to solve this. Maybe this can be a good challenge for you if you want to go even further.
:32 But for our use case, this is perfectly fine. I didn't encounter any problems at all. Great, ironic saying that I just received a failed fetch here, but that is from a different project. That's from the finished project. So, yes.
:49 Okay, amazing. So I'm gonna go ahead and merge this pull request. We are aware of those global shared instances and I will review that just to check if it's not something that will cause problems in our current implementation but I highly doubt it will. Great. Now that we've done that, let's go ahead and git checkout main, git pull origin main right here and once we've done that, We can always double check.
:18 We are on the main branch. Let's go ahead and open the graph. Here it is. We detached 11 and merged it back here. So in this chapter, we have successfully implemented GhostX suggestion, handle tab key suggestion acceptance.
:33 Let me go ahead and mark it. Implemented mod k quick edit model, fireclose scraping functions and all other selection based code editing features. Amazing amazing job and see you in the next chapter.