DEV Community

Cover image for How to build a markdown editor in two minutes (with GitHub Copilot)
Rizèl Scarlett for GitHub

Posted on • Edited on

How to build a markdown editor in two minutes (with GitHub Copilot)

Table of Contents

This blog post is long. Click this dropdown for a smoother navigation experience!

Is it possible to build a markdown editor in 2 minutes? It is for me (if I'm using GitHub Copilot). In fact, I proposed building a markdown editor with GitHub Copilot for a 15-minute demo, and my manager seemed a bit nervous that my idea was overly ambitious. However, he still entrusted me with the task. To my surprise, GitHub Copilot generated a markdown editor faster than I predicted. In this blog post, I’ll teach you how to build a markdown editor, too!

But before I do that, I want to manage readers' expectations.

  • This is a tutorial to help you understand how to practically use GitHub Copilot for day to day tasks.
  • The markdown editor we’re building will be more functional than stylistic. Translation: it might look a little ugly.
  • As a bonus, we’ll use Copilot Chat to improve the markdown editor’s user interface. However, this product is in technical preview and not yet available to everyone. If you don’t have access to it yet, you can substitute it for ChatGPT.
  • We will use the React Markdown npm package. When I first attempted this, GitHub Copilot imported the package without me prompting it, but it ended up being a helpful inclusion
  • You’ll need access to GitHub Copilot in your editor to get the best out of this tutorial!
  • GitHub Copilot and other Generative AI tools are non-deterministic. This means these tools will sometimes generate different outputs. You may receive slightly different output, but feel free to experiment with GitHub Copilot until you reach your desired outcome. Feel free to share what it created for you!
  • GitHub Copilot and other generative AI tools are advancing quickly. The advice in this tutorial is relevant for today – June 2023, but it might be outdated in the near future.

Use GitHub Copilot to build a markdown editor

Prerequisites:

Now that we have everything we need let’s get started!

Step 1: Create a Next.js GitHub Codespace template (30 seconds)

I use GitHub Codespace templates when I want to avoid installing boilerplate package dependencies and setting up my environment from scratch. It comes in handy when I want to spin up a quick proof of concept or deliver a demo. In our case, we want to save time installing Next.js and its accompanying dependencies.

You can create a Next.js GitHub Codespace template by navigating to https://github.com/codespaces/templates. Then choosing the “Use this template” button for Next.js. This will trigger a codespace to open with boilerplate Next.js code with a browser preview.

Highlights the use this template button for Nextjs

Step 2: Install specific dependencies (30 seconds)

Using a GitHub Codespace template reduced the need to install a few boilerplate dependencies, but we still have more to install.

  • Ensure the GitHub Copilot extension is installed. See here for instructions.

  • Install the React Markdown npm package. To do this, run the following command in your codespace’s terminal:
    npm install react-markdown

  • Install the styled-components npm package. To do this, by run the following command in your codespace's terminal:
    npm install styled-components@latest

Step 3: Delete the code in your index.js file (2 seconds)

Every GitHub Codespace template includes boilerplate code to empower developers in experimenting and swiftly prototyping their ideas. However, for our goals, we will write different code in our index.js file; we don't need the existing code.

Keep the index.js file, but delete the code inside.

Deleting code

Step 4: Prompt GitHub Copilot with a comment (4 seconds)

The comment below prompts GitHub Copilot to explain our overall goal and provide context to our AI programming assistant. Learn more about this method from my previous DEV post – A Beginner’s Guide to Prompt Engineering with GitHub Copilot. Copy the comment below and paste it at the top of your index.js file.

/*
Create a basic markdown editor in Next.js with the following features:
- Use React hooks
- Create state for markdown with the default text "## markdown preview"
- A text area where users can write markdown 
- Show a live preview of the markdown text as I type
- Support for basic markdown syntax like headers, bold, italics 
- Use React markdown npm package 
- The markdown text and resulting HTML should be saved in the component's state and updated in real time 
*/
Enter fullscreen mode Exit fullscreen mode

Step 5: Trigger suggestions from GitHub Copilot (18 seconds)

I pressed the RETURN key twice to trigger a suggestion from GitHub Copilot. I usually press the SPACEBAR, ENTER, or RETURN on my keyboard to trigger suggestions from GitHub Copilot after providing context.

receiving and accepting ghost text suggestions

💡 When you create a new line, you should see a suggestion, or ghost text, from GitHub Copilot.

💡 You can tell that GitHub Copilot will provide you with a suggestion when the Copilot icon in the status bar is spinning. I like to refer to it as "thinking". In the GIF below, I highlighted the Copilot icon, so you can see how it spins when processing.

Copilot thinking

💡 You can tell that GitHub Copilot is going to provide you with a suggestion when the Copilot icon in the status bar is spinning. I like to refer to it as “thinking”. In the GIF below, I highlighted the Copilot icon, so you can see how it spins when it is processing.

💡 Because the output for GitHub Copilot is non-deterministic, results may vary.

📝 Here's the GitHub Copilot gave me output:

import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import styled from 'styled-components';

const Index = () => {
  const [markdown, setMarkdown] = useState('## markdown preview');

  const handleChange = (e) => {
    setMarkdown(e.target.value);
  };

  return (
    <div>
      <textarea onChange={handleChange} value={markdown} />
      <ReactMarkdown>{markdown}</ReactMarkdown>
    </div>
  );
}

export default Index;
Enter fullscreen mode Exit fullscreen mode

✨ Feel free to tweak the output to your liking!

Step 6: Save the file and try it out! (30 seconds)

GitHub Copilot generated code that looks correct, but how do we know it works? Let's try writing in our markdown editor's input box to see if it renders an accurate live preview of the text.

I'll test the following markdown elements:

bullet points
- bullet
- bullet
- this is the markdown for bullets
- ...
Enter fullscreen mode Exit fullscreen mode

bold
I am **bold**
How to make a word **bold**: 
- sandwiched it between two asterisks on the left 
- and two asterisks on the right
Enter fullscreen mode Exit fullscreen mode

italic
I am *italic*
How to make a word *italic*: 
- sandwiched it between one asterisk on the left 
- and one asterisk on the right
Enter fullscreen mode Exit fullscreen mode

link
I am [link](google.com)
How to convert a word into a [link](google.com): 
- sandwiched it between an opening and closing bracket 
- then place an actual link next to it
- sandwich that link between two parentheses
Enter fullscreen mode Exit fullscreen mode

###### heading
I am  ###### heading
How to convert a word into a ##### heading: 
- Preface the word with hashtags 
- The more hashtags, the smaller the word gets
- More hashtags indicate the heading is less important
- # Heading 1 - I am super important.
- ## Heading 2 - I am a subtitle
- ### Heading 3 - I am less important
- #### Heading 4 - I am even smaller and less important
Enter fullscreen mode Exit fullscreen mode

Rendering markdown elements

💪🏾 We did it! We developed a markdown editor with GitHub Copilot in less than 2 minutes.

We did it Joe with Kamala Harris

Bonus: Use Copilot Chat to style, test, and explain your UI

That was fun, but we can do more. We can write unit tests, style our UI, and get an explanation of the code. We can do independently, but it's exponentially more fun to do pair program with GitHub Copilot for this.

To complete the second half of this tutorial, you will need access to Copilot Chat. Additionally, you need an active subscription to GitHub Copilot to access Copilot Chat. Learn more here.

Copilot Chat is a ChatGPT-like experience in your editor, but it’s not just a chat window. Because it’s deeply embedded into your IDE, Copilot Chat has context of what a developer types and what error messages a developer receives.

Let’s try using Copilot Chat to style, test, and explain the code in our markdown editor!

I feel comfortable with this framework, so I confidently accepted the previously generated code. However, you might be unfamiliar with React or Next.js, so the generated code looks foreign to you. This is a problem because you don’t want to blindly accept code. Understanding code that you will merge into a production environment is vital. Fortunately, Copilot Chat can provide an in-depth explanation of your code.

Step 1: Open the extension!

Following installation, try opening the extension. It should render a panel with a welcome message.

Welcome message in Copilot Chat

Step 2: Get an explanation of the generated code block

Let’s have Copilot Chat explain the handleChange function.

  • First, highlight the code block for the handleChange function
  • Right click after highlighting
  • Choose “Copilot”
  • Choose “Explain This”

Shows the generated explanation

After you follow the above steps, Copilot Chat should provide an explanation similar to the one below!

This code defines a function called handleChange that takes an event object as its argument. The handleChange function is typically used as an event handler for a textarea element in a React component. When the user types into the textarea, the handleChange function is called with an event object that contains information about the user's input.
The handleChange function uses the setMarkdown function to update the value of a state variable called markdown. The setMarkdown function is a function that is returned by the useState hook provided by the React library. The useState hook is used to add state to functional components in React. By using the setMarkdown function to update the markdown state variable, the component that contains this code can re-render itself to reflect the new state.
Overall, this code defines a function called handleChange that is used to update the value of a state variable called markdown in response to user input. The setMarkdown function is used to update the state variable, and the useState hook is used to add state to a functional component in React.

💡 You can repeat this step with any individual line of code or code block if you need more clarity.

💡 The more lines of code that you highlight, the less granular the explanation will be. If you want an overview, highlight the entire file, but if you want a granular explanation, highlight individual code blocks.

Step 3: Style your markdown editor

Let's communicate with Copilot Chat to guide us in styling our markdown editor.

✍🏿 Here’s the question that I asked Copilot Chat: “Can you propose code to make my markdown editor have a more modern UX design? Please use styled-components.”

Asking Copilot Chat a question

Step 4: Review Copilot Chat’s response

I want to re-emphasize that generative AI and GitHub Copilot have non-deterministic output, so results may vary. Copilot Chat proposed code that handles:

  • importing the styled-components npm package
  • styling the markdown editor's container
  • styling the text input
  • styling the markdown preview

See the generated code below:

import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import styled from 'styled-components';

const Container = styled.div`
  display: flex;
  flex-direction: column;
  height: 100vh;
  width: 100vw;
  align-items: center;
  justify-content: center;
  background-color: #f5f5f5;
`;

const TextArea = styled.textarea`
  width: 50%;
  height: 50%;
  padding: 1rem;
  border-radius: 5px;
  border: none;
  box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
  font-size: 1rem;
  resize: none;
`;

const Preview = styled(ReactMarkdown)`
  width: 50%;
  height: 50%;
  padding: 1rem;
  border-radius: 5px;
  border: none;
  box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
  font-size: 1rem;
  overflow-y: scroll;
  background-color: white;
`;

const Index = () => {
  const [markdown, setMarkdown] = useState('## markdown preview');

  const handleChange = e => {
    setMarkdown(e.target.value);
  };

  return (
    <Container>
      <TextArea value={markdown} onChange={handleChange} />
      <Preview>{markdown}</Preview>
    </Container>
  );
}

export default Index;
Enter fullscreen mode Exit fullscreen mode

I reviewed the code, and it looked like a feasible solution to me, so I copied the generated code and pasted it into my index.js file.

Step 5: Check out the results

Here’s what my final results looked like:

styled markdown editor

I guess this is Copilot’s version of a modern UX design, and it looks a bit better to me, but it could use some improvement. This is a great opportunity for you as the developer to tweak the code to match your vision!

Step 6: Keep experimenting

I spent over 20 minutes experimenting with Copilot Chat to improve my prompt game and understand Copilot’s limitations. I asked it different questions like:

  • How do I add a toolbar?
  • How do I add a dark mode and light mode theme?

Here’s what it came up with:

A markdown editor with a toolbar and dark mode

🧑🏾‍🎨 Get creative and make your markdown editor your own!

Step 7: Write unit tests with Copilot Chat

While writing unit tests is critical for any production application's longevity, it's also a really tedious process. I'm happy that the industry is moving towards the direction of automating away boilerplate code and repetitive code.

We can request that Copilot Chat generate unit tests for our markdown editor using slash commands.

After I typed "/tests" in Copilot Chat, I received proposed unit tests accompanied by a brief explanation. See the results below:

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Index from './index';

describe('Index', () => {
  it('renders the bold, italic, and theme toggle buttons', () => {
    const { getByText } = render(<Index />);
    expect(getByText('Bold')).toBeInTheDocument();
    expect(getByText('Italic')).toBeInTheDocument();
    expect(getByText('Dark Mode')).toBeInTheDocument();
  });

  it('toggles between dark and light mode when the theme toggle button is clicked', () => {
    const { getByText, container } = render(<Index />);
    const themeToggle = getByText('Dark Mode');
    expect(container.firstChild).toHaveStyle('background-color: #fff');
    fireEvent.click(themeToggle);
    expect(container.firstChild).toHaveStyle('background-color: #000');
    fireEvent.click(themeToggle);
    expect(container.firstChild).toHaveStyle('background-color: #fff');
  });

  it('updates the markdown text when the textarea is changed', () => {
    const { getByLabelText, getByText } = render(<Index />);
    const textarea = getByLabelText('Markdown Input');
    fireEvent.change(textarea, { target: { value: '# Hello World!' } });
    expect(getByText('# Hello World!')).toBeInTheDocument();
  });

  it('applies bold formatting to selected text when the bold button is clicked', () => {
    const { getByText, getByLabelText } = render(<Index />);
    const textarea = getByLabelText('Markdown Input');
    fireEvent.change(textarea, { target: { value: 'Hello **World**!' } });
    const boldButton = getByText('Bold');
    fireEvent.click(boldButton);
    expect(textarea.value).toBe('Hello **World**!');
    fireEvent.change(textarea, { target: { value: 'Hello **World!**' } });
    fireEvent.click(boldButton);
    expect(textarea.value).toBe('Hello World!');
  });

  it('applies italic formatting to selected text when the italic button is clicked', () => {
    const { getByText, getByLabelText } = render(<Index />);
    const textarea = getByLabelText('Markdown Input');
    fireEvent.change(textarea, { target: { value: 'Hello *World*!' } });
    const italicButton = getByText('Italic');
    fireEvent.click(italicButton);
    expect(textarea.value).toBe('Hello *World*!');
    fireEvent.change(textarea, { target: { value: 'Hello *World!*' } });
    fireEvent.click(italicButton);
    expect(textarea.value).toBe('Hello World!');
  });
});
Enter fullscreen mode Exit fullscreen mode

These tests cover the basic functionality of the component, including rendering the correct buttons, toggling between dark and light mode, updating the markdown text, and applying bold and italic formatting to selected text. Note that these tests assume that the TextArea and Preview components are working correctly, so you may want to write additional tests for those components as well.

Please note: I didn't try out the unit tests, so use at your own expense. I strongly recommend running the tests and reviewing the code before merging into a branch.

💾 ...And we're done! Experimenting with GitHub Copilot and Copilot Chat is always fun for me. For more content like this follow me or GitHub on DEV!

👀 Also, I'd love to hear your thoughts in the comments below.

Top comments (9)

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

I always find it a bit dishonest when articles are titled "How to do X" and the content turns out to be "using a library that already does X" plus some basic wireframe setup.

In the end, a more honest title might have been

How copilot can assist you in building a basic markdown editor using React markdown without worrying about the boilerplate (in half an hour or less)

And it's not like there isn't any merit in an article like that; but that just isn't the expectation when one reads the current title.

Collapse
 
blackgirlbytes profile image
Rizèl Scarlett • Edited

Sorry I disappointed you and I hear you, but I feel like I was honest. I even invested time into writing a whole managing expectations bullet point list for readers.

Also, that would be an extremely long title?? Plus, it does take two minutes. Like I've demoed this a bajillion times and it took me two minutes..so why would I put in half an hour?

I don't want to argue with you, but...

That's a very different perspective than mine because when I read tutorials..I'm not like expecting that we will do everything from scratch. In fact, I usually don't want to do anything from scratch lol. Is it the two minutes part that bothers you most? I remember working as an engineer feeling like what is the shortest way that I could get this accomplished. I've looked up a ton of markdown editor tutorials and MANY of them use React Markdown. Here's an example of one: storyblok.com/tp/build-markdown-ed...

My goal is to show how people can use GitHub Copilot for more practical tasks..like building a markdown editor. I feel like people can start taking these examples to be like how can I provide context to Copilot to import the packages I need etc etc. In fact, when I first created the markdown editor, I didn't even know it would import React Markdown..It just did, so I made it be part of the prompt/comment, so that it would import the package each time. And then I was excited and showed it to people. And they were all like "dang, it's crazy how quick you were able to do this". And then I was like you know what, let me turn this into a blog post, so people can get a better understanding of how to use Copilot.

I'm hoping people can say okay, now I'm getting a better understanding of how to write intentional comments to generate their target output.

Anyways, thanks for taking time to read the blog post and leave feedback. I know it's because you care!

Collapse
 
publicspace profile image
Donovan Martinez

Excellent.
Thank you for the article

Collapse
 
maharshi020 profile image
Maharshi020

Yea i agree. I want to see how github copilot can refactor a border line unreadable code with minimum unit test and rewrite it better. Development is 10 percent maintenance is the rest.

Collapse
 
jamonholmgren profile image
Jamon Holmgren

I really like these types of practical examples of using Copilot. Keep it up!

Collapse
 
blackgirlbytes profile image
Rizèl Scarlett

Thank you!!! Means a lot coming from you. I love your tweets on GitHub Copilot and other generative AI tools.

Collapse
 
shahriyardx profile image
Md Shahriyar Alam

At first I was thinking of a editor which shows the preview where you type. Not in a different container

Collapse
 
blackgirlbytes profile image
Rizèl Scarlett

You can tweak it and give Copilot a slightly different prompt than I did to generate the results you want. My post is just one example of what you can do.

Collapse
 
dyaskur profile image
Muhammad Dyas Yaskur

It looks amazing. I used copilot during free public preview, but not utilized like you. Now I have a plan use it again...