Как написать свой текстовый редактор

Привет, Хабр! Представляю вашему вниманию перевод статьи «Build Your Own Text Editor» автора Джереми Рутена.

Свой текстовый редактор!

Привет! Это вольный перевод о том, как написать свой никому не нужный текстовый редактор на C.

Итак, что сможет делать наш редактор?

  • Базовые функции редактора (писать, стирать, перемещать «каретку» и т.д.)
  • Искать по ключевому слову
  • Подсвечивать синтаксис

Еще чуть-чуть о реализации редактора:

  • Реализация редактора состоит из чуть больше, чем 1000 строк и одного файла
  • Не имеет зависимостей (кроме, конечно же, системных)

Всего туториал состоит из 184 шагов. Каждый шаг вы будете добавлять, изменять или удалять разные строки кода. На большинстве шагов вы сможете наблюдать за внесенными изменениями просто скомпилировав и запустив программу.

Я попробую объяснить каждый шаг на этом пути, иногда очень подробно. Не бойтесь пропускать теорию, потому что суть в самом написании кода и то, что вы вообще решились на это! Всё, что вы узнаете в процессе — это бонус, будет чему научится просто вводя изменения в код и наблюдая за результатами.

Настройки

Первый шаг, новый файл и всё всё всё с чистого листа…
Для начала убедитесь в том, что ваша среда корректно настроена для вашего ЯП (в нашем случае — для C), а так же вы хотя бы примерно понимаете, как компилировать и запускать вашу программу в этой среде.

К нашему счастью, редактор, который мы напишем не зависит от каких-либо внешних библиотек. Единственное, что нам понадобится, так это компилятор и стандартная библиотека языка C, с которой он и поставляется. Мы так же будем использовать make-скрипт для упрощения команд компиляции. Удостоверьтесь, что у вас есть как и компилятор для C, так и make.

Компилятор языка C

Так как C компилируемый язык, нам нужен, как не странно, компилятор, если у вас его нет, его обязательно нужно установить, как и make. Например, в Ubuntu, это можно сделать так:

sudo apt-get install gcc make

Функция main()

Итак, вот, с чего начинается наш путь! Создайте файл kilo.c и напишите в нем базовую main() функцию.

Шаг 1

int main() {
    return 0;
}

Для компиляции этого файла, достаточно ввести cc kilo.c -o kilo в ваш терминал. Если ошибок не последовало, в текущей директории появится исполняемый файл kilo. Что бы запустить программу, введите ./kilo. К сожалению, пока наша программа ничего не умеет, поэтому ничего нам не напечатает.

(не обязательно, что бы ваш редактор назывался так же, как и в статье, вы вполне можете назвать свой редактор «uber_mega_super_redaktor1337» другим эстетичным названием)

Упрощение компилирования с make

Каждый раз печатать что то вроде cc kilo.c -o kilo надоедает, поэтому мы и напишем make скрипт.

Создайте новый Makefile с подобным содержимым:

Шаг 2

kilo: kilo.c
    $(CC) kilo.c -o kilo -Wall -Wextra -pedantic -std=c99

Первая линия Makefile-а говорит нам о том, что мы хотим скомпилировать и то, что нам для этого потребуется. Вторая линия определяет команду, которую будет выполнять make скрипт. $(CC) обычно ссылается на команду cc.

Что это за магические слова появились?
Это флаги, а именно:

  1. -Wall — от английского «all Warnings», что говорит компилятору выводить почти все предупреждения, если ему что то не очень нравится.
  2. -Wextra и -pedantic просит компилятор выводить еще больше предупреждений, если такие имеются.
  3. -std=c99 показывает компилятору, какую версию стандарта языка C ему использовать при компилировании. Стандарт C99 немного упростит нам процесс написания кода.

Теперь, когда Makefile настроен, попробуйте написать команду make в ваш терминал для компиляции программы. Запустить её можно так же, как и всегда, ./kilo.

Продолжение следует…

Сделать собственный текстовый редактор гораздо проще, чем кажется. Сейчас мы соорудим такой, и вы сами в этом убедитесь.

В этой статье мы соберем текстовый редактор, который будет работать в браузере. В помощь нам три технологии:

  • JavaScript — язык, на котором всё будет работать. Это не лучший язык для полноценных приложений, но с ним вы сможете запрограммировать текстовый редактор прямо в браузере.
  • Contenteditable — возможность языка HTML делать какие-то части страницы редактируемыми. Обычно со страницы можно только читать, но благодаря этому свойству можно еще и писать.
  • Localstorage — особая область памяти, которая позволяет сохранить что-нибудь для конкретной страницы в браузере. Ваш браузер будет помнить, что вы ввели конкретно в этой странице. Это самая интересная часть.

Общая идея

У нас будет HTML-страница, на ней будет блок, похожий на лист бумаги. У него будет включен content editable, то есть внутри этого блока можно будет что-то писать. После каждого нажатия клавиши содержимое этого блока будет записываться во внутреннюю память браузера.

Вторая часть алгоритма — при загрузке страницы взять из памяти тот текст, что там был раньше, и показать его в нашей текстовой области. Страницу можно обновлять как угодно и даже выключать компьютер — текст всё равно останется в памяти.

Если записать алгоритм кратко, то он будет выглядеть так:

  1. Достаём из памяти тот текст, который там был
  2. Выводим его в нашу область для редактирования
  3. Постоянно смотрим, нажата ли какая-нибудь клавиша
  4. Если нажата — сразу записываем изменения в память.

Пункты 3 и 4 выполняются непрерывно до тех пор, пока вы не закроете страницу.

Готовим каркас

Пойдём уже привычным способом и создадим новый HTML-файл, в котором будут прописаны все нужные блоки. Мы так уже делали в статьях про генератор паролей и спортивный таймер.

<!DOCTYPE html>
<html>
<!-- служебная часть -->

<head>
  <!-- заголовок страницы -->
  <title>Текстовый редактор</title>
  <!-- настраиваем служебную информацию для браузеров -->
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- задаём CSS-стили прямо здесь же, чтобы всё было в одном файле -->
  <style type="text/css">
  </style>
  <!-- закрываем служебную часть страницы -->
</head>
<!-- началось содержимое страницы -->

<body>
  <!-- пишем скрипт, который будет постоянно сохранять наш текст -->
  <script>
  </script>
  <!-- закончилось содержимое страницы -->
</body>
<!-- конец всего HTML-документа -->

</html>

Сохраняем как html-файл, открываем его в браузере и видим пустой экран. Это нормально, сейчас будем наполнять.

Расставляем содержимое

Нам нужен только заголовок, который объяснит нам, где мы находимся, и большое пространство для ввода текста. За текстовое поле будет отвечать блок <div> со свойством contenteditable. Это свойство разрешает редактировать текст в блоке как угодно.

Разместим это в разделе <body>:

<!-- заголовок страницы -->
<h2>Текстовый редактор с автосохранением</h2>
<!-- большой блок для ввода текста: высота в половину, а ширина — во весь экран, назвывается "text_area", обведено рамкой толщиной в 1 пиксель, выравнивание текста — по левому краю -->
<div id="editor" contenteditable="true"
  style="height: 50%; width: 100%; border: solid; border-width: 1px; text-align: left">
</div>

Настраиваем стили

Стили задают внешний вид страницы и любых элементов на ней. Сделаем наш заголовок опрятнее:

/*задаём общие параметры для всей страницы: шрифт и отступы*/
body {
  text-align: center;
  margin: 10;
  font-family: Verdana, Arial, sans-serif;
  font-size: 16px;
}
/*закончили со стилями*/

Сохраняем, обновляем и смотрим на результат:

Пишем скрипт

Теперь нужно научить редактор сначала извлекать из памяти прошлый текст, а потом снова запоминать каждое нажатие клавиши. Всё будем делать через localStorage, как с ним работать — рассказываем в статье про список задач.

// если в нашем хранилище уже что-то есть…
if (localStorage.getItem('text_in_editor') !== null) {
  // …то отображаем его содержимое в нашем редакторе
  document.getElementById('editor').innerHTML = localStorage.getItem('text_in_editor');
}
// отслеживаем каждое нажатие клавиши и при каждом нажатии выполняем команду
document.addEventListener('keydown', function (e) {
  // записываем содержимое нашего редактора в хранилище
  localStorage.setItem('text_in_editor', document.getElementById('editor').innerHTML);
});

Кладём это в раздел <script> и смотрим, что получилось:

Общий код страницы

<!DOCTYPE html>
<html>
<!-- служебная часть -->

<head>
  <!-- заголовок страницы -->
  <title>Текстовый редактор</title>
  <!-- настраиваем служебную информацию для браузеров -->
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- задаём CSS-стили прямо здесь же, чтобы всё было в одном файле -->
  <style type="text/css">
    /*задаём общие параметры для всей страницы: шрифт и отступы*/
    body {
      text-align: center;
      margin: 10;
      font-family: Verdana, Arial, sans-serif;
      font-size: 16px;
    }

    /*закончили со стилями*/
  </style>
  <!-- закрываем служебную часть страницы -->
</head>
<!-- началось содержимое страницы -->

<body>
  <!-- началась видимая часть -->
  <!-- заголовок страницы -->
  <h2>Текстовый редактор с автосохранением</h2>
  <!-- большой блок для ввода текста: высота в половину, а ширина — во весь экран, назвывается "text_area", обведено рамкой толщиной в 1 пиксель, выравнивание текста — по левому краю -->
  <div id="editor" contenteditable="true"
    style="height: 50%; width: 100%; border: solid; border-width: 1px; text-align: left">
  </div>
  <!-- закончилась видимая часть -->
  <!-- пишем скрипт, который будет постоянно сохранять наш текст -->
  <script>
    // если в нашем хранилище уже что-то есть…
    if (localStorage.getItem('text_in_editor') !== null) {
      // …то отображаем его содержимое в нашем редакторе
      document.getElementById('editor').innerHTML = localStorage.getItem('text_in_editor');
    }
    // отслеживаем каждое нажатие клавиши и при каждом нажатии выполняем команду
    document.addEventListener('keydown', function (e) {
      // записываем содержимое нашего редактора в хранилище
      localStorage.setItem('text_in_editor', document.getElementById('editor').innerHTML);
    });
 // закончился скрипт
  </script>
  <!-- закончилось содержимое страницы -->
</body>
<!-- конец всего HTML-документа -->

</html>

В следующих сериях

Сделаем нашему редактору приличный внешний вид — чтобы был похож на лист бумаги с тенью.

Добавим возможность менять документы и создавать новые.

Добавим каждой заметке заголовок.

Подписывайтесь на наши соцсети, и как только выйдет новая версия, мы вам расскажем.

Рассмотрим пример проектирования стандартного оконного приложения. Простейшая последовательность действий:
1) визуальное проектирование интерфейса (перенос на форму с Панели элементов необходимых визуальных и не визуальных элементов);
2) генерация заготовок методов обработки событий, связанных с элементами управления;
3) программирование методов обработки событий.

Постановка задачи

Создать текстовый редактор с обязательными функциями работы с файлами  «Открыть» и «Сохранить как», а также функциями редактирования текста. Выбор действий с файлами осуществлять через главное меню.

Реализация

Разместим на форме визуальный элемент textBox1 класса TextBox. Размер элемента сделайте чуть меньше размера формы, сместив его вниз от заголовка на 30-40 пикселей. Задайте свойство textBox1.MultiLine = true (для редактирования текста в несколько строк).

Перетащите с Панели элементов компонент menuStrip1 класса MenuStrip для создания меню.В левом верхнем углу рабочей области формы появится кнопка «Введите здесь» , а на панели невизульных компонентов отобразится элемент menuStrip1.

Для выбора имен файлов для их чтения и записи перетащим на эту же панель элементы openFileDialog1 (класс OpenFileDialog) и saveFileDialog1 (класс SaveFileDialog).

Кликнув по кнопке «Введите здесь», введите имя раздела меню «Файл» и добавьте ниже следующие пункты меню работы с файлами «Открыть», «Сохранить как» и «Выход». Ваша форма (вместе с панелью невизуальных элементов) будет выглядеть примерно так:
Примечание: для наглядности изменено свойство формы BackColor = Color.Peru. Первая группа действий закончена.

Вторая группа действий обеспечивает генерацию  заголовков методов обработки событий, связанных к кнопками меню. Для этого дважды нажмите каждую из трех позиций меню, а также событию Load формы Form1 на закладке «События» панели «Свойства» поставьте в соответствие метод Form1_Load (двойной клик справа от в строке Load).

Откроем форму в режиме Кода (файл Form1.cs):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace ТекстовыйРедактор
{
   public partial class Form1 : Form
   {
      public Form1()
      {
         InitializeComponent();
      }

      private void открытьToolStripMenuItem_Click(object sender, EventArgs e)
      {

      }

      private void сохранитьКакToolStripMenuItem_Click(object sender,      EventArgs e)
      {

      }

      private void выходToolStripMenuItem_Click(object sender, EventArgs e)
      {

      }

      private void Form1_Load(object sender, EventArgs e)
      {

      }
   }
}

Перейдем к третьей группе действий — написанию кода для этих четырех методов.

Метод Form1_Load( ) используем для очистки поля компонента textBox1, для задания форматов файловых диалогов и имени файла — контрольного примера при его открытии:

private void Form1_Load(object sender, EventArgs e)
{
   textBox1.Clear();
   openFileDialog1.FileName = @"dataText2.txt";
   openFileDialog1.Filter =
            "Текстовые файлы (*.txt)|*.txt|All files (*.*)|*.*";
   saveFileDialog1.Filter =
            "Текстовые файлы (*.txt)|*.txt|All files (*.*)|*.*";
}

Комментарий. При загрузке формы мы задаем свойство FileName объекта openFileDialog1 указанием имени файла для открытия, а также задаем фильтры для диалогов открытия и сохранения файлов.  Сравните работу программы без использования этого метода.

В методе  открытьToolStripMenuItem_Click( ) используется  компонент openFileDialog1 для выбора имени файла для чтения. Если имя не выбрано (FileName = String.Empty), то работа метода завершается. Иначе создается новый экземпляр класса System.IO.StreamReader (var Читатель) с указанием имени файла и кодировки, данные из текстового файла переносятся в textBox1, объект Читатель закрывается. Добавлена обработка исключений,  которые могут возникнуть при открытии файла:

private void открытьToolStripMenuItem_Click(object sender, EventArgs e)
{
   openFileDialog1.ShowDialog();
   if (openFileDialog1.FileName == String.Empty) return;
   // Чтение текстового файла
   try
   {
      var Читатель = new System.IO.StreamReader(
      openFileDialog1.FileName, Encoding.GetEncoding(1251));
      textBox1.Text = Читатель.ReadToEnd();
      Читатель.Close();
   }
   catch (System.IO.FileNotFoundException Ситуация)
   {
      MessageBox.Show(Ситуация.Message + "nНет такого файла",
               "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
   }
   catch (Exception Ситуация)
   { // отчет о других ошибках
      MessageBox.Show(Ситуация.Message,
           "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
   }
}

Аналогично выполняется запись (сохранение) текстового файла:

private void сохранитьКакToolStripMenuItem_Click(object sender,EventArgs e)
{
   saveFileDialog1.FileName = openFileDialog1.FileName;
   if (saveFileDialog1.ShowDialog() == DialogResult.OK)
   {
      try
      {
         var Писатель = new System.IO.StreamWriter(
         saveFileDialog1.FileName, false,              
                             System.Text.Encoding.GetEncoding(1251));
         Писатель.Write(textBox1.Text);
         Писатель.Close(); 
     }
     catch (Exception Ситуация)
     { // отчет о других ошибках
          MessageBox.Show(Ситуация.Message,
              "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
     }
  }
}

Последний метод — закрытие формы — реализуется одним оператором:

private void выходToolStripMenuItem_Click(object sender, EventArgs e)
{
   this.Close();
}

Подготовим в блокноте текстовый файл Text2.txt и разместим его в подкаталоге data папки, где будет размещено ваше приложение.  Запустим программу на выполнение.   Добавьте в окне редактора несколько строк, сохраните файл с другим именем. Откройте новый файл и удалите часть текста в окне редактора.

Заметим, что работают стандартные операции выделения текста, копирования, вставки и удаления части текста с использованием мыши и комбинаций управляющих клавиш, используемых в известных текстовых редакторах.

ВЫВОД: В первом приближении поставленная задача решена. Данный пример приведен всего лишь для понимания:
во-первых,  алгоритма действий разработчика визуального приложения  «интерфейс, события-методы, реализация»;
во-вторых, удобства использования готовых компонентов (нам не пришлось программировать операции редактирования текста — стандартные события уже привязаны к компоненту textBox1);
в-третьих, целесообразности использования готовых компонентов (файловых диалогов) с точки зрения стандартизации интерфейса пользователя и программы.

 
Напомним, что в статье про классы мы отмечали, что в языке C# предусмотрено несколько разновидностей данных-членов и функций-членов. Пока мы подробно рассмотрели только поля и константы — как данные-члены, а также методы — как функции-члены класса. В следующих статьях мы рассмотрим события, как данные-члены класса и трехзвенную цепочку события-делегаты-методы. После чего вернемся к член-функциям класса: свойствам, конструкторам, финализаторам, операциям и индексаторам.


NEW: Наш Чат, в котором вы можете обсудить любые вопросы, идеи, поделиться опытом или связаться с администраторами.


Помощь проекту:

Предлагаем вашему вниманию серию англоязычных статей, опубликованную в блоге Джереми Рутена. В ней подробно разбирается процесс разработки собственного текстового редактора на языке C.

Проект занимает примерно тысячу строк кода и не использует никаких зависимостей. Для удобства и лучшего понимания весь процесс разбит на 184 шага. По прохождении каждого этапа вы сможете скомпилировать проект и увидеть все изменения. Исходный код каждого шага доступен на GitHub.

Статьи написаны простым, понятным даже начинающим программистам языком. Все термины либо объясняются в статье, либо снабжены ссылками на другие Интернет-ресурсы.

Реализация этого проекта однозначно будет вам полезна: во-первых, вы сможете освоить или подтянуть знания по языку C, а во-вторых, разберётесь в основных принципах работы текстовых редакторов.

Итак, вот список статей:

  1. Настройка.
  2. Переход в «сырой» режим.
  3. «Сырой» ввод и вывод.
  4. Отображение текста.
  5. Редактор текста.
  6. Поиск.
  7. Подсветка синтаксиса.
  8. Примечания.

Если вам хочется написать ещё какой-нибудь проект самостоятельно, советуем обратить внимание на серию статей, посвящённую созданию ОС на ассемблере и Rust.

  • Download source files — 14.2 Kb
  • Download demo project — 5.57 Kb

Project Screenshot

Introduction

Want to start programming in Visual C++? how about making a simple «text editor» even simpler than notepad for a start?

Here is a step by step guide which will guide you through every step of the way, in my humble opinion, this is one of the best guides
on making a simple text editor with MFC.

In this article you will learn the basics of Visual C++ and how to use Button, Edit box, and Dialog box controls and their various options.

Enjoy…

Creating the Application

  • Start Visual C++.
  • Go to File -> New.
  • Click on Projects, if it isn’t already selected.
  • Select MFC AppWizard (exe), type a «Project name» and click OK.
  • Choose «Dialog based» and click Finish, then click OK.
  • Delete the TODO: Place dialog controls here. static text.
    (by clicking on it once and pressing the delete key on the keyboard)
  • And also delete the OK and Cancel buttons.
  • Now go to Project -> Add To Project -> Components And Controls…
  • Then click on «Registered ActiveX Controls»
  • Find «Microsoft Common Dialog Control» and double click it.
  • Click OK.
  • And click OK again.
  • Then click Close.
  • A new icon should now appear. Image 2
  • Click that, then click on the dialog.
  • Resize the thing that appears, make it a bit smaller.
  • Right click on the dialog, and select «Properties».
  • Go to «Styles» check «Minimize box», then click on the [x].
  • Resize the dialog around how big you want the text editor window to be.
  • Move the «Common Dialog» thing to the bottom.
  • Click on the button icon, then click near the bottom of the dialog.
  • Right click the button that appeared and select Properties.
  • Select «General» if it isn’t already selected.
  • Then delete the Button1 text for the «Caption» and type &Clear.
  • Delete the 1 on IDC_BUTTON1 and type _CLEAR, renaming it to IDC_BUTTON_CLEAR, for the ID, then click [x].
  • Use those same steps to make a button called &Open... and Save &As... using the appropriate ID.
  • Now click on the ab| icon then draw an «Edit Box» from the top left of the dialog
    to all the way to the right of the dialog then draw it downwards towards the buttom
    but don’t draw past the buttons.
  • Click on the massive Edit box that you drew (once), then right click it and select Properties.
  • Select Styles, uncheck «Auto HScroll».
  • Check «Multiline».
  • And check «Vertical scroll».
  • Also check «Auto VScroll».
  • Finally check «Want return» and click the [x].
  • Now it’s time to test out what you did so far, go to Build -> Execute -> OK.

    It works, you can type stuff like any text editor, but you can’t save or open files just yet, that bit you have to program which I explain bellow.

    Using the code

    To add opening and saving files functionality to the text editor, you’ll need some functions that open files in reading and writing mode, here are some functions which I copied and pasted from my program, najitool GUI which you can use for this text editor program as well. Double click on an empty space on the dialog and paste the following code under the #ifdef _DEBUG to #endif stuff.

    #ifdef _DEBUG
    #define new DEBUG_NEW
    #undef THIS_FILE
    static char THIS_FILE[] = __FILE__;
    #endif

    So paste the following code right under the above code…

    #include <stdio.h>
    #include <stdlib.h>
     
    
    FILE *naji_input;
    FILE *naji_output;
    CString naji_buffer;
    
    
    void najin(const char *namein)
    {
    
        naji_input = fopen(namein, "rb");
    
        if (naji_input == NULL)
        {
        naji_buffer.Format("Error, cannot open input file: %s", namein);
        MessageBox(0, naji_buffer, "Error", 0);
        exit(2);
        }
    
    }
    
    
    void najout(const char *nameout)
    {
    
        naji_output = fopen(nameout, "wb");
    
        if (naji_output == NULL)
        {
        naji_buffer.Format("Error, cannot open output file: %s", nameout);
        MessageBox(0, naji_buffer, "Error", 0);
        exit(4);
        }
    
    }

    Now close the window with all the code, to go back to the resource thing with the dialog and stuff.

  • Right click the dialog and select «ClassWizard».
  • Select «Member Variables» if it isn’t already selected.
  • Select IDC_COMMONDIALOG1, and click «Add Variable».
  • Name the «Member variable name» as m_dialogbox and click OK.
  • Now select IDC_EDIT1, and click «Add Variable».
  • Name the «Member variable name» as m_buffer and click OK.
  • Click OK again.
  • Double click the massive Edit box, click Ok, it should make a function called, OnChangeEdit1.
  • Paste the following code under the comment
    TODO: Add your control notification handler code here

    .

    UpdateData(TRUE);
    

    You need to do this so the member variable m_buffer for the Edit box gets updated everytime the user types something in the Edit box.

  • Close the code window to go back to the resource thing.
  • Double click the «Clear» button, click OK, it should make a function called, OnButtonClear.
  • Now paste the following code under the comment TODO: Add your control notification handler code here
    .

    m_buffer="";
    UpdateData(FALSE);
    

    Now the «Clear» button is fully functional, for the user to clear all the text that they typed, they just click the «Clear» button.
    Test it out, go to Build -> Execute, and click OK.

  • Close the code window if you already haven’t to go back to the resource thing.
  • Double click the «Open» button, click OK, it should make a function called, OnButtonOpen.
  • Paste the following code under the comment TODO: Add your control notification handler code here.
    int a;
    CString filename;
    
        m_dialogbox.SetDialogTitle("Open");
        m_dialogbox.SetFilter("Text Files|*.txt|All Files|*.*");
        m_dialogbox.ShowOpen();
    
        if (m_dialogbox.GetFileName().IsEmpty() == FALSE) 
        filename = m_dialogbox.GetFileName(); 
        else return;
    
        OnButtonClear();
        najin(filename);
    
        while(1)
        {
        a = fgetc(naji_input);
        
        if (a == EOF) break;
    
        m_buffer += (char) a;
        }
    
        UpdateData(FALSE);
    
        fclose(naji_input);

    This should make the «Open» button functionly as well now, test it out if you want,
    remember how to build and execute a program? Build -> Execute, and click OK.

  • Close the code window if you already haven’t to go back to the resource thing again.
  • Double click the «Save As» button, click OK, it should make a function called, OnButtonSaveAs.
  • Finally paste the following code under the comment TODO: Add your control notification handler code here
    .

    CString filename;
        
        m_dialogbox.SetDialogTitle("Save As"); 
        m_dialogbox.SetFilter("Text Files|*.txt|All Files|*.*"); 
        m_dialogbox.ShowOpen(); 
    
        if (m_dialogbox.GetFileName().IsEmpty() == FALSE) 
        filename = m_dialogbox.GetFileName(); 
        else return;
    
        najout(filename);
    
        fputs(m_buffer, naji_output);
        fclose(naji_output);

    That’s all there is to it, all the buttons are now fully functional, and the simple text editor is complete.
    Try them all out, Build -> Execute -> OK. Don’t forget to save your work now, File -> Save All.

  • License

    This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

    A list of licenses authors might use can be found here

    Written By

    Web Developer

    United Kingdom United Kingdom

    Necdet got his first computer in 1996 at the age of 11, a 386 at 3MHz, with 4MB RAM and a 10MB hard disk. His next computer was a 486 which he got 6 months later, he was shown how to remove and install hardware on that computer. Then he got a Pentium one year after that, and then a Pentium 3 in 2000 which he still uses. He learned programming in 1999, when he finally had access to the Internet and was able to find tutorials on C++, he then found tutorials on ANSI standard C, and didn’t see the point in C++ so he used ANSI C instead for all his programs. He made lots of small but useful programs (almost 100) until 2000, but with an unfortunate hard disk crash, all his programs were lost which made him very dishearted about programming. He done no programming whatsoever until 2003, when he decided that people could learn programming from his simple programs so he tried to remember all the programs he wrote in 1999-2000 which were lost in the hard disk crash, and rewrite them from scratch, as a single program having all the functions of all of them. He called this program najitool, and posted the first version on sourceforge in 2004 with 9 functions he rewrote which he could remember. In 2005, he realized that most people didn’t use command line programs, so he decided to make a Windows GUI (Graphical User Interface) version of najitool called najitool GUI a.k.a. naji_gui, written in Visual C++ 6.0 using MFC.

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

    • Download source files — 14.2 Kb
    • Download demo project — 5.57 Kb

    Project Screenshot

    Introduction

    Want to start programming in Visual C++? how about making a simple «text editor» even simpler than notepad for a start?

    Here is a step by step guide which will guide you through every step of the way, in my humble opinion, this is one of the best guides
    on making a simple text editor with MFC.

    In this article you will learn the basics of Visual C++ and how to use Button, Edit box, and Dialog box controls and their various options.

    Enjoy…

    Creating the Application

  • Start Visual C++.
  • Go to File -> New.
  • Click on Projects, if it isn’t already selected.
  • Select MFC AppWizard (exe), type a «Project name» and click OK.
  • Choose «Dialog based» and click Finish, then click OK.
  • Delete the TODO: Place dialog controls here. static text.
    (by clicking on it once and pressing the delete key on the keyboard)
  • And also delete the OK and Cancel buttons.
  • Now go to Project -> Add To Project -> Components And Controls…
  • Then click on «Registered ActiveX Controls»
  • Find «Microsoft Common Dialog Control» and double click it.
  • Click OK.
  • And click OK again.
  • Then click Close.
  • A new icon should now appear. Image 2
  • Click that, then click on the dialog.
  • Resize the thing that appears, make it a bit smaller.
  • Right click on the dialog, and select «Properties».
  • Go to «Styles» check «Minimize box», then click on the [x].
  • Resize the dialog around how big you want the text editor window to be.
  • Move the «Common Dialog» thing to the bottom.
  • Click on the button icon, then click near the bottom of the dialog.
  • Right click the button that appeared and select Properties.
  • Select «General» if it isn’t already selected.
  • Then delete the Button1 text for the «Caption» and type &Clear.
  • Delete the 1 on IDC_BUTTON1 and type _CLEAR, renaming it to IDC_BUTTON_CLEAR, for the ID, then click [x].
  • Use those same steps to make a button called &Open... and Save &As... using the appropriate ID.
  • Now click on the ab| icon then draw an «Edit Box» from the top left of the dialog
    to all the way to the right of the dialog then draw it downwards towards the buttom
    but don’t draw past the buttons.
  • Click on the massive Edit box that you drew (once), then right click it and select Properties.
  • Select Styles, uncheck «Auto HScroll».
  • Check «Multiline».
  • And check «Vertical scroll».
  • Also check «Auto VScroll».
  • Finally check «Want return» and click the [x].
  • Now it’s time to test out what you did so far, go to Build -> Execute -> OK.

    It works, you can type stuff like any text editor, but you can’t save or open files just yet, that bit you have to program which I explain bellow.

    Using the code

    To add opening and saving files functionality to the text editor, you’ll need some functions that open files in reading and writing mode, here are some functions which I copied and pasted from my program, najitool GUI which you can use for this text editor program as well. Double click on an empty space on the dialog and paste the following code under the #ifdef _DEBUG to #endif stuff.

    #ifdef _DEBUG
    #define new DEBUG_NEW
    #undef THIS_FILE
    static char THIS_FILE[] = __FILE__;
    #endif

    So paste the following code right under the above code…

    #include <stdio.h>
    #include <stdlib.h>
     
    
    FILE *naji_input;
    FILE *naji_output;
    CString naji_buffer;
    
    
    void najin(const char *namein)
    {
    
        naji_input = fopen(namein, "rb");
    
        if (naji_input == NULL)
        {
        naji_buffer.Format("Error, cannot open input file: %s", namein);
        MessageBox(0, naji_buffer, "Error", 0);
        exit(2);
        }
    
    }
    
    
    void najout(const char *nameout)
    {
    
        naji_output = fopen(nameout, "wb");
    
        if (naji_output == NULL)
        {
        naji_buffer.Format("Error, cannot open output file: %s", nameout);
        MessageBox(0, naji_buffer, "Error", 0);
        exit(4);
        }
    
    }

    Now close the window with all the code, to go back to the resource thing with the dialog and stuff.

  • Right click the dialog and select «ClassWizard».
  • Select «Member Variables» if it isn’t already selected.
  • Select IDC_COMMONDIALOG1, and click «Add Variable».
  • Name the «Member variable name» as m_dialogbox and click OK.
  • Now select IDC_EDIT1, and click «Add Variable».
  • Name the «Member variable name» as m_buffer and click OK.
  • Click OK again.
  • Double click the massive Edit box, click Ok, it should make a function called, OnChangeEdit1.
  • Paste the following code under the comment
    TODO: Add your control notification handler code here

    .

    UpdateData(TRUE);
    

    You need to do this so the member variable m_buffer for the Edit box gets updated everytime the user types something in the Edit box.

  • Close the code window to go back to the resource thing.
  • Double click the «Clear» button, click OK, it should make a function called, OnButtonClear.
  • Now paste the following code under the comment TODO: Add your control notification handler code here
    .

    m_buffer="";
    UpdateData(FALSE);
    

    Now the «Clear» button is fully functional, for the user to clear all the text that they typed, they just click the «Clear» button.
    Test it out, go to Build -> Execute, and click OK.

  • Close the code window if you already haven’t to go back to the resource thing.
  • Double click the «Open» button, click OK, it should make a function called, OnButtonOpen.
  • Paste the following code under the comment TODO: Add your control notification handler code here.
    int a;
    CString filename;
    
        m_dialogbox.SetDialogTitle("Open");
        m_dialogbox.SetFilter("Text Files|*.txt|All Files|*.*");
        m_dialogbox.ShowOpen();
    
        if (m_dialogbox.GetFileName().IsEmpty() == FALSE) 
        filename = m_dialogbox.GetFileName(); 
        else return;
    
        OnButtonClear();
        najin(filename);
    
        while(1)
        {
        a = fgetc(naji_input);
        
        if (a == EOF) break;
    
        m_buffer += (char) a;
        }
    
        UpdateData(FALSE);
    
        fclose(naji_input);

    This should make the «Open» button functionly as well now, test it out if you want,
    remember how to build and execute a program? Build -> Execute, and click OK.

  • Close the code window if you already haven’t to go back to the resource thing again.
  • Double click the «Save As» button, click OK, it should make a function called, OnButtonSaveAs.
  • Finally paste the following code under the comment TODO: Add your control notification handler code here
    .

    CString filename;
        
        m_dialogbox.SetDialogTitle("Save As"); 
        m_dialogbox.SetFilter("Text Files|*.txt|All Files|*.*"); 
        m_dialogbox.ShowOpen(); 
    
        if (m_dialogbox.GetFileName().IsEmpty() == FALSE) 
        filename = m_dialogbox.GetFileName(); 
        else return;
    
        najout(filename);
    
        fputs(m_buffer, naji_output);
        fclose(naji_output);

    That’s all there is to it, all the buttons are now fully functional, and the simple text editor is complete.
    Try them all out, Build -> Execute -> OK. Don’t forget to save your work now, File -> Save All.

  • License

    This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

    A list of licenses authors might use can be found here

    Written By

    Web Developer

    United Kingdom United Kingdom

    Necdet got his first computer in 1996 at the age of 11, a 386 at 3MHz, with 4MB RAM and a 10MB hard disk. His next computer was a 486 which he got 6 months later, he was shown how to remove and install hardware on that computer. Then he got a Pentium one year after that, and then a Pentium 3 in 2000 which he still uses. He learned programming in 1999, when he finally had access to the Internet and was able to find tutorials on C++, he then found tutorials on ANSI standard C, and didn’t see the point in C++ so he used ANSI C instead for all his programs. He made lots of small but useful programs (almost 100) until 2000, but with an unfortunate hard disk crash, all his programs were lost which made him very dishearted about programming. He done no programming whatsoever until 2003, when he decided that people could learn programming from his simple programs so he tried to remember all the programs he wrote in 1999-2000 which were lost in the hard disk crash, and rewrite them from scratch, as a single program having all the functions of all of them. He called this program najitool, and posted the first version on sourceforge in 2004 with 9 functions he rewrote which he could remember. In 2005, he realized that most people didn’t use command line programs, so he decided to make a Windows GUI (Graphical User Interface) version of najitool called najitool GUI a.k.a. naji_gui, written in Visual C++ 6.0 using MFC.

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

    Подробностями разработки онлайн-платформы выполнения и компиляции кода более чем на 40 языках делимся к старту курса по Frontend-разработке. Автор этого материала — основатель TailwindMasterKit.


    Онлайн-платформа выполнения кода позволяет писать и сразу запускать код на любимом языке программирования. В идеале можно увидеть вывод программы, например двоичного поиска на JavaScript.

    Демонстрации

    • Исходный код.

    • Интерактивная демоверсия.

    Создадим функциональный редактор кода Monaco Editor. Вот его возможности:

    • поддержка VS Code;

    • компиляция в веб-приложении со стандартным вводом и выводом и поддержкой более чем 40 языков;

    • выбор темы редактора из списка доступных тем;

    • информация о коде (время выполнения, используемая память, статус и т. д.).

    Технологический стек

    • React.js для фронтенда;

    • TailwindCSS для стилей;

    • Judge0 для компиляции и выполнения кода;

    • RapidAPI для быстрого развёртывания кода Judge0;

    • Monaco Editor — редактор кода для проекта.

    Структура проекта

    Структура проекта проста:

    • сomponents: компоненты / сниппеты кода (например, CodeEditorWindow и Landing);

    • hooks: пользовательские хуки (и хуки нажатия клавиш — для компилирования кода с помощью событий клавиатуры);

    • lib: библиотечные функции (здесь создадим функцию определения темы);

    • constants: константы, такие как languageOptions и customStyles, для выпадающих списков;

    • utils: служебные функции для сопровождения кода.

    Логика работы с приложением

    Прежде чем переходить к коду, разберёмся в логике работы с приложением и в том, как писать для него код с нуля.

    • Пользователь попадает в веб-приложение и выбирает язык (по умолчанию — JavaScript).

    • После написания кода пользователь его компилирует, а выходные данные просматривает в окне вывода.

    • В окне вывода кода вы увидите вывод и статус кода.

    • Пользователь может добавлять к фрагментам кода свои входные данные, которые учитываются в judge (онлайн-компиляторе).

    • Пользователь может видеть информацию о выполненном коде (пример: на компиляцию и выполнение ушло 5 мс, использовано 2024 Кб памяти, выполнение кода завершено успешно).

    Ознакомившись со структурой каталогов проекта и логикой работы с приложением, перейдём к коду и разберёмся, как тут всё организовано.

    Как создать компонент редактора кода

    Компонент редактора кода состоит из Monaco Editor, то есть настраиваемого NPM-пакета:

    // CodeEditorWindow.js
    
    import React, { useState } from "react";
    
    import Editor from "@monaco-editor/react";
    
    const CodeEditorWindow = ({ onChange, language, code, theme }) => {
      const [value, setValue] = useState(code || "");
    
      const handleEditorChange = (value) => {
        setValue(value);
        onChange("code", value);
      };
    
      return (
        <div className="overlay rounded-md overflow-hidden w-full h-full shadow-4xl">
          <Editor
            height="85vh"
            width={`100%`}
            language={language || "javascript"}
            value={value}
            theme={theme}
            defaultValue="// some comment"
            onChange={handleEditorChange}
          />
        </div>
      );
    };
    export default CodeEditorWindow;

    Компоненты Editor берутся из пакета @monaco-editor/react, который позволяет развернуть редактор кода с соответствующей высотой области просмотра 85vh.

    Компонент Editor принимает много свойств:

    • language: язык, для которого нужны подсветка синтаксиса и автодополнение ввода.

    • theme: цвета и фон фрагмента кода (настроим позже).

    • value: код, который вводится в редактор.

    • onChange: происходит при изменении value в редакторе. Изменившееся значение нужно сохранить в состоянии, чтобы позже для компиляции вызвать API Judge0.

    Редактор получает свойства onChange, language, code и theme родительского компонента Landing.js. Когда в редакторе меняется свойство value, вызываем обработчик onChange из родительского компонента Landing.

    Как создать компонент Landing

    Компонент landing в состоит из трёх частей:

    • Actions Bar с компонентами выпадающих списков Languages и Themes.

    • Компонент Code Editor Window.

    • Компоненты Output и Custom Input.

    // Landing.js
    
    import React, { useEffect, useState } from "react";
    import CodeEditorWindow from "./CodeEditorWindow";
    import axios from "axios";
    import { classnames } from "../utils/general";
    import { languageOptions } from "../constants/languageOptions";
    
    import { ToastContainer, toast } from "react-toastify";
    import "react-toastify/dist/ReactToastify.css";
    
    import { defineTheme } from "../lib/defineTheme";
    import useKeyPress from "../hooks/useKeyPress";
    import Footer from "./Footer";
    import OutputWindow from "./OutputWindow";
    import CustomInput from "./CustomInput";
    import OutputDetails from "./OutputDetails";
    import ThemeDropdown from "./ThemeDropdown";
    import LanguagesDropdown from "./LanguagesDropdown";
    
    const javascriptDefault = `// some comment`;
    
    const Landing = () => {
      const [code, setCode] = useState(javascriptDefault);
      const [customInput, setCustomInput] = useState("");
      const [outputDetails, setOutputDetails] = useState(null);
      const [processing, setProcessing] = useState(null);
      const [theme, setTheme] = useState("cobalt");
      const [language, setLanguage] = useState(languageOptions[0]);
    
      const enterPress = useKeyPress("Enter");
      const ctrlPress = useKeyPress("Control");
    
      const onSelectChange = (sl) => {
        console.log("selected Option...", sl);
        setLanguage(sl);
      };
    
      useEffect(() => {
        if (enterPress && ctrlPress) {
          console.log("enterPress", enterPress);
          console.log("ctrlPress", ctrlPress);
          handleCompile();
        }
      }, [ctrlPress, enterPress]);
      const onChange = (action, data) => {
        switch (action) {
          case "code": {
            setCode(data);
            break;
          }
          default: {
            console.warn("case not handled!", action, data);
          }
        }
      };
      const handleCompile = () => {
        // We will come to the implementation later in the code
      };
    
      const checkStatus = async (token) => {
        // We will come to the implementation later in the code
      };
    
      function handleThemeChange(th) {
        // We will come to the implementation later in the code
      }
      useEffect(() => {
        defineTheme("oceanic-next").then((_) =>
          setTheme({ value: "oceanic-next", label: "Oceanic Next" })
        );
      }, []);
    
      const showSuccessToast = (msg) => {
        toast.success(msg || `Compiled Successfully!`, {
          position: "top-right",
          autoClose: 1000,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: true,
          progress: undefined,
        });
      };
      const showErrorToast = (msg) => {
        toast.error(msg || `Something went wrong! Please try again.`, {
          position: "top-right",
          autoClose: 1000,
          hideProgressBar: false,
          closeOnClick: true,
          pauseOnHover: true,
          draggable: true,
          progress: undefined,
        });
      };
    
      return (
        <>
          <ToastContainer
            position="top-right"
            autoClose={2000}
            hideProgressBar={false}
            newestOnTop={false}
            closeOnClick
            rtl={false}
            pauseOnFocusLoss
            draggable
            pauseOnHover
          />
          <div className="h-4 w-full bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500"></div>
          <div className="flex flex-row">
            <div className="px-4 py-2">
              <LanguagesDropdown onSelectChange={onSelectChange} />
            </div>
            <div className="px-4 py-2">
              <ThemeDropdown handleThemeChange={handleThemeChange} theme={theme} />
            </div>
          </div>
          <div className="flex flex-row space-x-4 items-start px-4 py-4">
            <div className="flex flex-col w-full h-full justify-start items-end">
              <CodeEditorWindow
                code={code}
                onChange={onChange}
                language={language?.value}
                theme={theme.value}
              />
            </div>
    
            <div className="right-container flex flex-shrink-0 w-[30%] flex-col">
              <OutputWindow outputDetails={outputDetails} />
              <div className="flex flex-col items-end">
                <CustomInput
                  customInput={customInput}
                  setCustomInput={setCustomInput}
                />
                <button
                  onClick={handleCompile}
                  disabled={!code}
                  className={classnames(
                    "mt-4 border-2 border-black z-10 rounded-md shadow-[5px_5px_0px_0px_rgba(0,0,0)] px-4 py-2 hover:shadow transition duration-200 bg-white flex-shrink-0",
                    !code ? "opacity-50" : ""
                  )}
                >
                  {processing ? "Processing..." : "Compile and Execute"}
                </button>
              </div>
              {outputDetails && <OutputDetails outputDetails={outputDetails} />}
            </div>
          </div>
          <Footer />
        </>
      );
    };
    export default Landing;

    Рассмотрим базовую структуру Landing подробнее.

    Компонент CodeEditorWindow

    Как мы уже видели, в компоненте CodeEditorWindow учитываются постоянно меняющийся код и метод onChange, с помощью которого отслеживаются изменения в код:.

    // onChange method implementation
    
     const onChange = (action, data) => {
        switch (action) {
          case "code": {
            setCode(data);
            break;
          }
          default: {
            console.warn("case not handled!", action, data);
          }
        }
      };

    Задаём состояние code и отслеживаем изменения.

    В компоненте CodeEditorWindow также учитывается свойство language — выбранный в данный момент язык, для которого нужны подсветка синтаксиса и автодополнение ввода.

    Массив languageOptions я создал для отслеживания принятых в Monaco Editor свойств языка, а также для работы с компиляцией (отслеживаем languageId, принимаемый в этих API judge0):

    // constants/languageOptions.js
    
    export const languageOptions = [
      {
        id: 63,
        name: "JavaScript (Node.js 12.14.0)",
        label: "JavaScript (Node.js 12.14.0)",
        value: "javascript",
      },
      {
        id: 45,
        name: "Assembly (NASM 2.14.02)",
        label: "Assembly (NASM 2.14.02)",
        value: "assembly",
      },
        ...
        ...
        ...
        ...
        ...
        ...
        
      {
        id: 84,
        name: "Visual Basic.Net (vbnc 0.0.0.5943)",
        label: "Visual Basic.Net (vbnc 0.0.0.5943)",
        value: "vbnet",
      },
    ];

    В каждом объекте languageOptions есть свойства id, name, label и value. Массив languageOptions помещается в выпадающий список и предоставляются как его варианты.

    Когда состояние выпадающего списка меняется, в методе onSelectChange отслеживается выбранный id с соответствующим изменением состояния.

    Компонент LanguageDropdown

    // LanguageDropdown.js
    
    import React from "react";
    import Select from "react-select";
    import { customStyles } from "../constants/customStyles";
    import { languageOptions } from "../constants/languageOptions";
    
    const LanguagesDropdown = ({ onSelectChange }) => {
      return (
        <Select
          placeholder={`Filter By Category`}
          options={languageOptions}
          styles={customStyles}
          defaultValue={languageOptions[0]}
          onChange={(selectedOption) => onSelectChange(selectedOption)}
        />
      );
    };
    
    export default LanguagesDropdown;

    Для выпадающих списков и их обработчиков изменений используется пакет react-select.

    Основные параметры react-select — defaultValue и массив options (здесь будем передавать languageOptions), с помощью которого автоматически отображаются все эти значения выпадающего списка.

    Свойство defaultValue — это указываемое в компоненте значение по умолчанию. Языком по умолчанию оставим первый язык в массиве языков — JavaScript.

    Когда пользователь меняет язык, это происходит с помощью onSelectChange:

    const onSelectChange = (sl) => {
        setLanguage(sl);
    };

    Компонент ThemeDropdown

    Компонент ThemeDropdown очень похож на LanguageDropdown (с пользовательским интерфейсом и пакетом react-select):

    // ThemeDropdown.js
    
    import React from "react";
    import Select from "react-select";
    import monacoThemes from "monaco-themes/themes/themelist";
    import { customStyles } from "../constants/customStyles";
    
    const ThemeDropdown = ({ handleThemeChange, theme }) => {
      return (
        <Select
          placeholder={`Select Theme`}
          // options={languageOptions}
          options={Object.entries(monacoThemes).map(([themeId, themeName]) => ({
            label: themeName,
            value: themeId,
            key: themeId,
          }))}
          value={theme}
          styles={customStyles}
          onChange={handleThemeChange}
        />
      );
    };
    
    export default ThemeDropdown;

    Здесь для выбора красивых тем из списка ниже, доступных Monaco Editor, используем пакет monacoThemes:

    // lib/defineTheme.js
    
    import { loader } from "@monaco-editor/react";
    
    const monacoThemes = {
      active4d: "Active4D",
      "all-hallows-eve": "All Hallows Eve",
      amy: "Amy",
      "birds-of-paradise": "Birds of Paradise",
      blackboard: "Blackboard",
      "brilliance-black": "Brilliance Black",
      "brilliance-dull": "Brilliance Dull",
      "chrome-devtools": "Chrome DevTools",
      "clouds-midnight": "Clouds Midnight",
      clouds: "Clouds",
      cobalt: "Cobalt",
      dawn: "Dawn",
      dreamweaver: "Dreamweaver",
      eiffel: "Eiffel",
      "espresso-libre": "Espresso Libre",
      github: "GitHub",
      idle: "IDLE",
      katzenmilch: "Katzenmilch",
      "kuroir-theme": "Kuroir Theme",
      lazy: "LAZY",
      "magicwb--amiga-": "MagicWB (Amiga)",
      "merbivore-soft": "Merbivore Soft",
      merbivore: "Merbivore",
      "monokai-bright": "Monokai Bright",
      monokai: "Monokai",
      "night-owl": "Night Owl",
      "oceanic-next": "Oceanic Next",
      "pastels-on-dark": "Pastels on Dark",
      "slush-and-poppies": "Slush and Poppies",
      "solarized-dark": "Solarized-dark",
      "solarized-light": "Solarized-light",
      spacecadet: "SpaceCadet",
      sunburst: "Sunburst",
      "textmate--mac-classic-": "Textmate (Mac Classic)",
      "tomorrow-night-blue": "Tomorrow-Night-Blue",
      "tomorrow-night-bright": "Tomorrow-Night-Bright",
      "tomorrow-night-eighties": "Tomorrow-Night-Eighties",
      "tomorrow-night": "Tomorrow-Night",
      tomorrow: "Tomorrow",
      twilight: "Twilight",
      "upstream-sunburst": "Upstream Sunburst",
      "vibrant-ink": "Vibrant Ink",
      "xcode-default": "Xcode_default",
      zenburnesque: "Zenburnesque",
      iplastic: "iPlastic",
      idlefingers: "idleFingers",
      krtheme: "krTheme",
      monoindustrial: "monoindustrial",
    };
    
    const defineTheme = (theme) => {
      return new Promise((res) => {
        Promise.all([
          loader.init(),
          import(`monaco-themes/themes/${monacoThemes[theme]}.json`),
        ]).then(([monaco, themeData]) => {
          monaco.editor.defineTheme(theme, themeData);
          res();
        });
      });
    };
    
    export { defineTheme };

    В monaco-themes тем много, так что внешний вид будущего редактора — не проблема.

    Темы выбирает функция defineTheme, в ней возвращается промис, посредством которого с помощью экшена monaco.editor.defineTheme(theme, themeData) задаётся тема редактора. Само изменение тем внутри окна кода Monaco Editor происходит в этой строке кода.

    Функция defineTheme вызывается с помощью обратного вызова onChange, который мы уже видели в компоненте ThemeDropdown.js:

    // Landing.js - handleThemeChange() function
    
    function handleThemeChange(th) {
        const theme = th;
        console.log("theme...", theme);
    
        if (["light", "vs-dark"].includes(theme.value)) {
          setTheme(theme);
        } else {
          defineTheme(theme.value).then((_) => setTheme(theme));
        }
      }
      

    В функции handleThemeChange() проверяется тема: light (светлая) или dark (тёмная). Эти темы по умолчанию доступны в компоненте MonacoEditor — вызывать метод defineTheme() не нужно.

    Если тем в списке нет, вызываем компонент defineTheme() и задаём состояние выбранной темы.

    Как компилировать код с помощью Judge0

    Перейдём к самой «вкусной» части приложения — компиляции кода на разных языках, для которой используем Judge0 — интерактивную систему выполнения кода.

    Выполнить вызов API можно с произвольными параметрами (исходный код, идентификатор языка) и получить в ответ выходные данные.

    Настраиваем Judge0:

    • переходим к Judge0 и выбираем базовый план;

    • на самом деле Judge0 размещён на RapidAPI (идём дальше и подписываемся на базовый план);

    • после этого можно скопировать RAPIDAPI_HOST и RAPIDAPI_KEY (для выполнения вызовов API в систему выполнения кода).

    Дашборд выглядит так:

    Для вызовов API нужны параметры X-RapidAPI-Host и X-RapidAPI-Key. Сохраните их в файлах .env:

    REACT_APP_RAPID_API_HOST = YOUR_HOST_URL
    REACT_APP_RAPID_API_KEY = YOUR_SECRET_KEY
    REACT_APP_RAPID_API_URL = YOUR_SUBMISSIONS_URL

    В React важно инициализировать переменные окружения с префиксом REACT_APP.

    Будем использовать URL-адрес SUBMISSIONS_URL из хоста и маршрута /submission.

    Например, https://judge0-ce.p.rapidapi.com/submissions будет URL-адресом submissions в нашем случае.

    После настройки переменных переходим к логике компиляции.

    Логика и последовательность компиляции

    Последовательность компиляции следующая:

    • Нажатие кнопки Compile and Execute вызывает метод handleCompile().

    • В функции handleCompile() вызывается бэкенд Judge0 RapidAPI по URL-адресу submissions с указанием в качестве параметров запроса — languageId, source_code и stdin — в нашем случае customInput.

    • В options как заголовки также принимаются host и secret.

    • Могут передаваться дополнительные параметры base64_encoded и fields.

    • При отправке POST-запроса submission наш запрос регистрируется на сервере, и создаётся процесс. Ответ на POST-запрос — token, необходимый для проверки статуса выполнения (Processing, Accepted, Time Limit Exceeded, Runtime Exceptions и др.).

    • По возвращении успешность результатов можно проверить с помощью условий, а затем показать результаты в окне вывода.

    Разберём метод handleCompile():

    const handleCompile = () => {
        setProcessing(true);
        const formData = {
          language_id: language.id,
          // encode source code in base64
          source_code: btoa(code),
          stdin: btoa(customInput),
        };
        const options = {
          method: "POST",
          url: process.env.REACT_APP_RAPID_API_URL,
          params: { base64_encoded: "true", fields: "*" },
          headers: {
            "content-type": "application/json",
            "Content-Type": "application/json",
            "X-RapidAPI-Host": process.env.REACT_APP_RAPID_API_HOST,
            "X-RapidAPI-Key": process.env.REACT_APP_RAPID_API_KEY,
          },
          data: formData,
        };
    
        axios
          .request(options)
          .then(function (response) {
            console.log("res.data", response.data);
            const token = response.data.token;
            checkStatus(token);
          })
          .catch((err) => {
            let error = err.response ? err.response.data : err;
            setProcessing(false);
            console.log(error);
          });
      };

    Он принимает languageId, source_code и stdin. Обратите внимание на btoa перед source_code и stdin. Это нужно для кодирования строк в формате base64, потому что у нас в параметрах запроса к API есть base64_encoded: true.

    Если получен успешный ответ и есть token, вызываем метод checkStatus() для опроса маршрута /submissions/${token}:

    const checkStatus = async (token) => {
        const options = {
          method: "GET",
          url: process.env.REACT_APP_RAPID_API_URL + "/" + token,
          params: { base64_encoded: "true", fields: "*" },
          headers: {
            "X-RapidAPI-Host": process.env.REACT_APP_RAPID_API_HOST,
            "X-RapidAPI-Key": process.env.REACT_APP_RAPID_API_KEY,
          },
        };
        try {
          let response = await axios.request(options);
          let statusId = response.data.status?.id;
    
          // Processed - we have a result
          if (statusId === 1 || statusId === 2) {
            // still processing
            setTimeout(() => {
              checkStatus(token)
            }, 2000)
            return
          } else {
            setProcessing(false)
            setOutputDetails(response.data)
            showSuccessToast(`Compiled Successfully!`)
            console.log('response.data', response.data)
            return
          }
        } catch (err) {
          console.log("err", err);
          setProcessing(false);
          showErrorToast();
        }
      };

    Чтобы получить результаты отправленного ранее кода, нужно опросить submissions с помощью token из ответа. Для этого выполняем GET-запрос к конечной точке. После получения ответа проверяем statusId === 1 || statusId === 2. Но что это значит? У нас 14 статусов, связанных с любой отправляемой в API частью кода:

    export const statuses = [
      {
        id: 1,
        description: "In Queue",
      },
      {
        id: 2,
        description: "Processing",
      },
      {
        id: 3,
        description: "Accepted",
      },
      {
        id: 4,
        description: "Wrong Answer",
      },
      {
        id: 5,
        description: "Time Limit Exceeded",
      },
      {
        id: 6,
        description: "Compilation Error",
      },
      {
        id: 7,
        description: "Runtime Error (SIGSEGV)",
      },
      {
        id: 8,
        description: "Runtime Error (SIGXFSZ)",
      },
      {
        id: 9,
        description: "Runtime Error (SIGFPE)",
      },
      {
        id: 10,
        description: "Runtime Error (SIGABRT)",
      },
      {
        id: 11,
        description: "Runtime Error (NZEC)",
      },
      {
        id: 12,
        description: "Runtime Error (Other)",
      },
      {
        id: 13,
        description: "Internal Error",
      },
      {
        id: 14,
        description: "Exec Format Error",
      },
    ];

    Если statusId === 1 или statusId === 2, код обрабатывается, и нужно снова вызвать API и проверить, получен ли результат. Из-за этого в if прописан setTimeout(), где снова вызывается функция checkStatus(), а внутри неё снова вызывается API и проверяется статус.

    Если статус не 2 или 3, выполнение кода завершено и есть результат — успешно скомпилированный код или код с превышением предела времени компиляции. А может, код с исключением времени выполнения; statusId представляет все ситуации, которые тоже можно воспроизвести.

    Например, в while(true) выдаётся ошибка превышения предела времени:

    Или, если допущена ошибка синтаксиса, вернётся ошибка компиляции:

    Так или иначе, есть результат, который сохраняется в состоянии outputDetails, чтобы было что отображать в окне вывода, в правой части экрана.

    Компонент окна вывода

    import React from "react";
    
    const OutputWindow = ({ outputDetails }) => {
      const getOutput = () => {
        let statusId = outputDetails?.status?.id;
    
        if (statusId === 6) {
          // compilation error
          return (
            <pre className="px-2 py-1 font-normal text-xs text-red-500">
              {atob(outputDetails?.compile_output)}
            </pre>
          );
        } else if (statusId === 3) {
          return (
            <pre className="px-2 py-1 font-normal text-xs text-green-500">
              {atob(outputDetails.stdout) !== null
                ? `${atob(outputDetails.stdout)}`
                : null}
            </pre>
          );
        } else if (statusId === 5) {
          return (
            <pre className="px-2 py-1 font-normal text-xs text-red-500">
              {`Time Limit Exceeded`}
            </pre>
          );
        } else {
          return (
            <pre className="px-2 py-1 font-normal text-xs text-red-500">
              {atob(outputDetails?.stderr)}
            </pre>
          );
        }
      };
      return (
        <>
          <h1 className="font-bold text-xl bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-700 mb-2">
            Output
          </h1>
          <div className="w-full h-56 bg-[#1e293b] rounded-md text-white font-normal text-sm overflow-y-auto">
            {outputDetails ? <>{getOutput()}</> : null}
          </div>
        </>
      );
    };
    
    export default OutputWindow;

    Это простой компонент для отображения успеха или неуспеха компиляции. В методе getOutput() определяются вывод и цвет текста.

    • Если statusId равен 3, имеем успешный сценарий со статусом Accepted. От API возвращается stdout — Standard Output («Стандартный вывод»). Он нужен для отображения данных, возвращаемых из отправленного в API кода.

    • Если statusId равен 5, имеем ошибку превышения предела времени. Просто показываем, что в коде есть условие бесконечного цикла или превышено стандартное время выполнения кода 5 секунд.

    • Если statusId равен 6, имеем ошибку компиляции. В этом случае API возвращает compile_output с возможностью отображения ошибки.

    • При любом другом статусе получаем стандартный объект stderr для отображения ошибок.

    • Обратите внимание: используется метод atob(), потому что выходные данные — это строка в base64. Тот же метод нужен, чтобы декодировать её.

    Вот успешный сценарий программы двоичного поиска на JavaScript:

    Компонент вывода подробностей

    Компонент OutputDetails — это простой модуль сопоставления для вывода данных, связанных с изначально скомпилированным фрагментом кода. Данные уже заданы в переменной состояния outputDetails:

    import React from "react";
    
    const OutputDetails = ({ outputDetails }) => {
      return (
        <div className="metrics-container mt-4 flex flex-col space-y-3">
          <p className="text-sm">
            Status:{" "}
            <span className="font-semibold px-2 py-1 rounded-md bg-gray-100">
              {outputDetails?.status?.description}
            </span>
          </p>
          <p className="text-sm">
            Memory:{" "}
            <span className="font-semibold px-2 py-1 rounded-md bg-gray-100">
              {outputDetails?.memory}
            </span>
          </p>
          <p className="text-sm">
            Time:{" "}
            <span className="font-semibold px-2 py-1 rounded-md bg-gray-100">
              {outputDetails?.time}
            </span>
          </p>
        </div>
      );
    };
    
    export default OutputDetails;

    time, memory и status.description читаются из ответа от API, а затем сохраняются в outputDetails и отображаются.

    События клавиатуры

    И последнее — ctrl+enter для компиляции. Чтобы прослушивать в веб-приложении события клавиатуры, создаётся пользовательский хук, крутой и намного чище:

    // useKeyPress.js
    
    import React, { useState } from "react";
    
    const useKeyPress = function (targetKey) {
      const [keyPressed, setKeyPressed] = useState(false);
    
      function downHandler({ key }) {
        if (key === targetKey) {
          setKeyPressed(true);
        }
      }
    
      const upHandler = ({ key }) => {
        if (key === targetKey) {
          setKeyPressed(false);
        }
      };
    
      React.useEffect(() => {
        document.addEventListener("keydown", downHandler);
        document.addEventListener("keyup", upHandler);
    
        return () => {
          document.removeEventListener("keydown", downHandler);
          document.removeEventListener("keyup", upHandler);
        };
      });
    
      return keyPressed;
    };
    
    export default useKeyPress;
    // Landing.js
    
    ...
    ...
    ...
    const Landing = () => {
        ...
        ...
          const enterPress = useKeyPress("Enter");
          const ctrlPress = useKeyPress("Control");
       ...
       ...
    }

    Здесь для прослушивания целевой клавиши нужны нативные прослушиватели событий JavaScript. События keydown и keyup прослушиваются с помощью хука. Хук инициализируется целевой клавишей Enter и Control. Проверяется targetKey === key и, соответственно, задаётся keyPressed, поэтому можно использовать возвращаемое логическое значение keyPressed — true или false.

    Теперь можно прослушать эти события в хуке useEffect и убедиться, что обе клавиши нажаты одновременно:

    useEffect(() => {
        if (enterPress && ctrlPress) {
          console.log("enterPress", enterPress);
          console.log("ctrlPress", ctrlPress);
          handleCompile();
        }
      }, [ctrlPress, enterPress]);

    Метод handleCompile() вызывается, когда пользователь нажимает Ctrl и Enter последовательно или одновременно.

    Что нужно учитывать

    Работать было интересно, но базовый план Judge0 о.ограничен, например, сотней запросов в день. Чтобы обойти ограничения, можно поднять собственный сервер/дроплет (на Digital Ocean) и разместить проект с открытым исходным кодом на своём хостинге, документация для этого отличная.

    Заключение

    В итоге у нас появился:

    • редактор кода, способный компилировать более 40 языков;

    • переключатель тем;

    • API — интерактивные и размещаемые на RapidAPI;

    • прослушивание событий клавиатуры через кастомные хуки React;

    • и много всего интересного!

    Хотите поработать над проектом плотнее? Подумайте над реализацией такого функционала:

    • Модуль авторизации и регистрации — для сохранения кода в собственном дашборде.

    • Способ совместного использования кода через Интернет.

    • Страница и настройки профиля.

    • Работа вдвоём над одним фрагментом кода с использованием программирования сокетов и операционных преобразований.

    • Закладки для фрагментов кода.

    • Пользовательский дашборд с сохранением, как CodePen.

    Мне очень понравилось писать код этого приложения с нуля. TailwindCSS — абсолютный фаворит и любимый ресурс для стилизации приложений. Если статья оказалась полезной, оставьте звезду в репозитории GitHub. Есть вопросы? Свяжитесь со мной в Twitter и/или на сайте, буду рад помочь.

    А мы поможем вам прокачать навыки или с самого начала освоить профессию, востребованную в любое время:

    • Профессия Frontend-разработчик

    • Профессия Fullstack-разработчик на Python

    Выбрать другую востребованную профессию.

    Понравилась статья? Поделить с друзьями:
  • Как написать свой сценарий для фильма
  • Как написать свой почтовый клиент
  • Как написать свой стишок
  • Как написать свой почтовый адрес на английском
  • Как написать свой стиллер