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

Rizèl Scarlett - Jun 17 '23 - - Dev Community

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .