/*
 * Copyright (c) 2010, 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is designed to work with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms, as
 * designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have either included with
 * the program or referenced in the documentation.
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 
 */

#include <unordered_set>

#include "base/log.h"
#include "base/drawing.h"
#include "base/string_utilities.h"
#include "base/file_utilities.h"
#include "base/notifications.h"
#include "base/drawing.h"
#include "base/xml_functions.h"

#include "SciLexer.h"

#include "mforms/mforms.h"
#include "mforms/utilities.h"

#include "mysql/MySQLRecognizerCommon.h"
#include "SymbolTable.h"

DEFAULT_LOG_DOMAIN(DOMAIN_MFORMS_BE)

using namespace mforms;
using namespace base;

// Marker ID assignments. Markers with higher number overlay lower ones.
// Note: the order here matches the LineMarkup enum, so we can directly use the enum as marker flags.
//       The LineMarkup is a bitmask (so we can create what Scintilla calls a marker set).
#define CE_STATEMENT_MARKER 0
#define CE_ERROR_MARKER 1
#define CE_BREAKPOINT_MARKER 2
#define CE_BREAKPOINT_HIT_MARKER 3
#define CE_CURRENT_LINE_MARKER 4
#define CE_ERROR_CONTINUE_MARKER 5

#define AC_LIST_SEPARATOR '\x19' // Unused codes as separators.
#define AC_TYPE_SEPARATOR '\x18'

#define ERROR_INDICATOR INDIC_CONTAINER
#define ERROR_INDICATOR_VALUE 42 // Arbitrary value.

typedef struct {
  bool hiresImage;
  int width;
  int height;
  void *data;
} ImageRecord;
static std::map<std::string, ImageRecord> registeredImages; // Registered RGBA images used in all editors.

//----------------- CodeEditorConfig -----------------------------------------------------------------------------------

CodeEditorConfig::CodeEditorConfig(SyntaxHighlighterLanguage language) {
  _used_language = language;
  _xmlDocument = nullptr;
  _xmlLanguageElement = nullptr;

  std::string lexer;
  std::string override_lexer;
  switch (language) {
    case mforms::LanguageMySQL56:
      override_lexer = "SCLEX_MYSQL_56";
      lexer = "SCLEX_MYSQL";
      break;

    case mforms::LanguageMySQL57:
      override_lexer = "SCLEX_MYSQL_57";
      lexer = "SCLEX_MYSQL";
      break;

    case mforms::LanguageMySQL80:
      override_lexer = "SCLEX_MYSQL_80";
      lexer = "SCLEX_MYSQL";
      break;

    case mforms::LanguageHtml:
      lexer = "SCLEX_HTML";
      break;

    case mforms::LanguagePython:
      lexer = "SCLEX_PYTHON";
      break;

    case mforms::LanguageCpp:
      lexer = "SCLEX_CPP";
      break;

    case mforms::LanguageJS:
      lexer = "SCLEX_CPP";
      override_lexer = "SCLEX_CPP_JS";
      break;

    case mforms::LanguageJson:
      lexer = "SCLEX_CPP";
      override_lexer = "SCLEX_CPP_JSON";
      break;

    default:
      return;
  }

  // Load the user's config file if it exists, otherwise use the default one.
  std::string config_file =
    mforms::Utilities::get_special_folder(mforms::ApplicationData) + "/MySQL/Workbench/code_editor.xml";

  if (!base::file_exists(config_file))
    config_file = App::get()->get_resource_path("") + "/data/code_editor.xml";

  try {
    _xmlDocument = base::xml::loadXMLDoc(config_file);
    if (_xmlDocument == nullptr) {
      logError("Code Editor Config: cannot load configuration file \"%s\"\n", config_file.c_str());
      return;
    }
  } catch (std::runtime_error& re) {
    logError("Code Editor Config: cannot load configuration file \"%s\":\n\t%s \n", config_file.c_str(), re.what());
    return;
  }

  auto rootElement = base::xml::getXmlRoot(_xmlDocument);
  if (!base::xml::nameIs(rootElement, "languages")) {
    logError("Code Editor: invalid configuration file \"%s\"\n", config_file.c_str());
    return;
  }

  {
    auto current = rootElement->children;

    // Load the available language identifiers. All remaining values are loaded on demand.
    while (current != nullptr) {
      if (base::xml::nameIs(current, "language")) {
        std::string languageName = base::xml::getProp(current, "name");
        if (languageName == lexer)
          _xmlLanguageElement = current;
        _languages.push_back(languageName);
      }
      current = current->next;
    }
  }

  if (_xmlLanguageElement == nullptr) {
    logWarning(
      "Code Editor: could not find settings for language %s in configuration file "
      "\"%s\"\n",
      lexer.c_str(), config_file.c_str());
    return;
  }

  parse_properties();
  parse_settings();
  parse_keywords();
  parse_styles();

  // check if there's another config section containing values that should override the base one
  if (!override_lexer.empty() && override_lexer != lexer) {
    bool found = false;
    // Load the available language identifiers. All remaining values are loaded on demand.

    auto current = rootElement->children;
    while (current != nullptr) {
      if (base::xml::nameIs(current, "language")) {
        std::string languageName = base::xml::getProp(current, "name");
        if (languageName == override_lexer) {
          _xmlLanguageElement = current;
          found = true;
          break;
        }
      }
      current = current->next;
    }

    if (found) {
      parse_properties();
      parse_settings();
      parse_keywords();
      parse_styles();
    }
  }
}

//----------------------------------------------------------------------------------------------------------------------

CodeEditorConfig::~CodeEditorConfig() {
  if (_xmlDocument != nullptr)
    xmlFree(_xmlDocument);
  _xmlDocument = nullptr;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditorConfig::parse_properties() {
  auto current = _xmlLanguageElement->children;
  while (current != nullptr) {
    if (base::xml::nameIs(current, "property")) {
      std::string pName = base::xml::getProp(current, "name");
      std::string pValue = base::xml::getProp(current, "value");
      if (!pName.empty() && !pValue.empty())
        _properties[pName] = pValue;
    }
    current = current->next;
  }
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditorConfig::parse_settings() {
  auto current = _xmlLanguageElement->children;
  while (current != nullptr) {
    if (base::xml::nameIs(current, "setting")) {
      std::string pName = base::xml::getProp(current, "name");
      std::string pValue = base::xml::getProp(current, "value");
      if (!pName.empty() && !pValue.empty())
        _settings[pName] = pValue;
    }
    current = current->next;
  }
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditorConfig::parse_keywords() {
  auto current = _xmlLanguageElement->children;
  while (current != nullptr) {
    if (base::xml::nameIs(current, "keywords")) {
      std::string pName = base::xml::getProp(current, "name");
      std::string text = base::xml::getContentRecursive(current);
      if (!pName.empty() && !text.empty())
        _keywords[pName] = text;
    }
    current = current->next;
  }
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditorConfig::parse_styles() {
  auto current = _xmlLanguageElement->children;
  while (current != nullptr) {
    if (base::xml::nameIs(current, "style")) {
      int id = (int)std::strtol(base::xml::getProp(current, "id").c_str(), nullptr, 10);
      if (id < 0) {
        current = current->next;
        continue;
      }

      std::map<std::string, std::string> entries;
      auto attribute = current->properties;
      while (attribute != nullptr) {
        if (base::xml::nameIs(attribute, "id")) {
          attribute = attribute->next;
          continue;
        }

        entries[std::string((const char*)attribute->name)] = base::xml::getContent(attribute->children);
        attribute = attribute->next;
      }
      _styles[id] = entries;
    }
    current = current->next;
  }
}

//----------------- CodeEditor -----------------------------------------------------------------------------------------

CodeEditor::CodeEditor(void* host, bool showInfo) : _host(host) {
  _code_editor_impl = &ControlFactory::get_instance()->_code_editor_impl;

  _code_editor_impl->create(this, showInfo);
  _code_editor_impl->send_editor(this, SCI_SETCODEPAGE, SC_CP_UTF8, 0);
  _context_menu = NULL;
  _find_panel = NULL;
  _scroll_on_resize = true;
  _auto_indent = false;

  scoped_connect(Form::main_form()->signal_deactivated(), std::bind(&CodeEditor::auto_completion_cancel, this));
  base::NotificationCenter::get()->add_observer(this, "GNColorsChanged");

  // Load the marker images for folding, debugging, error marker etc.
  setupMarker(CE_STATEMENT_MARKER, "editor_statement.png");
  setupMarker(CE_ERROR_MARKER, "editor_error.png");
  setupMarker(CE_BREAKPOINT_MARKER, "editor_breakpoint.png");
  setupMarker(CE_BREAKPOINT_HIT_MARKER, "editor_breakpoint_hit.png");
  setupMarker(CE_CURRENT_LINE_MARKER, "editor_current_pos.png");
  setupMarker(CE_ERROR_CONTINUE_MARKER, "editor_continue_on_error.png");

  // Determine fixed settings that don't change with the OS appearance.
  // Margin 0: line numbers.
  _code_editor_impl->send_editor(this, SCI_SETMARGINTYPEN, 0, SC_MARGIN_NUMBER);
#if defined(__APPLE__)
  _code_editor_impl->send_editor(this, SCI_STYLESETSIZE, STYLE_LINENUMBER, static_cast<sptr_t>(10));
#else
  _code_editor_impl->send_editor(this, SCI_STYLESETSIZE, STYLE_LINENUMBER, static_cast<sptr_t>(8));
#endif
  sptr_t lineNumberStyleWidth =
  _code_editor_impl->send_editor(this, SCI_TEXTWIDTH, STYLE_LINENUMBER, (sptr_t) "_9999");
  _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 0, lineNumberStyleWidth);
  _code_editor_impl->send_editor(this, SCI_SETMARGINSENSITIVEN, 0, 0);

  // Margin 1: all kind of markers (debugging, current line, code line indicator etc.).
  _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 1, 16);
  _code_editor_impl->send_editor(this, SCI_SETMARGINSENSITIVEN, 1, 1);

  // Margin 2: folding.
  _code_editor_impl->send_editor(this, SCI_SETPROPERTY, (uptr_t) "fold", (sptr_t) "1");
  _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 2, 13);
  _code_editor_impl->send_editor(this, SCI_SETAUTOMATICFOLD, SC_AUTOMATICFOLD_SHOW | SC_AUTOMATICFOLD_CHANGE, 0);
  _code_editor_impl->send_editor(this, SCI_SETMARGINMASKN, 2, SC_MASK_FOLDERS);
  _code_editor_impl->send_editor(this, SCI_SETMARGINSENSITIVEN, 2, 1); // Margin is clickable.

  _code_editor_impl->send_editor(this, SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPEN, SC_MARK_CIRCLEMINUS);
  _code_editor_impl->send_editor(this, SCI_MARKERDEFINE, SC_MARKNUM_FOLDER, SC_MARK_CIRCLEPLUS);
  _code_editor_impl->send_editor(this, SCI_MARKERDEFINE, SC_MARKNUM_FOLDERSUB, SC_MARK_VLINE);
  _code_editor_impl->send_editor(this, SCI_MARKERDEFINE, SC_MARKNUM_FOLDERTAIL, SC_MARK_LCORNERCURVE);
  _code_editor_impl->send_editor(this, SCI_MARKERDEFINE, SC_MARKNUM_FOLDEREND, SC_MARK_CIRCLEPLUSCONNECTED);
  _code_editor_impl->send_editor(this, SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPENMID, SC_MARK_CIRCLEMINUSCONNECTED);
  _code_editor_impl->send_editor(this, SCI_MARKERDEFINE, SC_MARKNUM_FOLDERMIDTAIL, SC_MARK_TCORNERCURVE);

  // Margin 3: empty, to provide some spacing between the folder margin and the text.
  _code_editor_impl->send_editor(this, SCI_SETMARGINTYPEN, 3, SC_MARGIN_BACK);
  _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 3, 5);
  _code_editor_impl->send_editor(this, SCI_SETMARGINSENSITIVEN, 3, 0);

  // Init markers & indicators for highlighting of syntax errors.
  _code_editor_impl->send_editor(this, SCI_INDICSETUNDER, ERROR_INDICATOR, 1);
  _code_editor_impl->send_editor(this, SCI_INDICSETSTYLE, ERROR_INDICATOR, INDIC_SQUIGGLE);

  // Other settings.
  _code_editor_impl->send_editor(this, SCI_SETEXTRAASCENT, 3, 0);
  _code_editor_impl->send_editor(this, SCI_SETEXTRADESCENT, 3, 0);

  _code_editor_impl->send_editor(this, SCI_SETCARETLINEVISIBLE, 1, 0);
  _code_editor_impl->send_editor(this, SCI_SETCARETWIDTH, 2, 0);

  // - Tabulators + indentation
  _code_editor_impl->send_editor(this, SCI_SETTABINDENTS, 1, 0);
  _code_editor_impl->send_editor(this, SCI_SETBACKSPACEUNINDENTS, 1, 0);

  // - Call tips
  _code_editor_impl->send_editor(this, SCI_SETMOUSEDWELLTIME, 200, 0);

  // - Line ending + scrolling.
  _code_editor_impl->send_editor(this, SCI_SETSCROLLWIDTHTRACKING, 1, 0);
  _code_editor_impl->send_editor(this, SCI_SETEOLMODE, SC_EOL_LF, 0);

  // - Auto completion
  _code_editor_impl->send_editor(this, SCI_AUTOCSETSEPARATOR, AC_LIST_SEPARATOR, 0);
  _code_editor_impl->send_editor(this, SCI_AUTOCSETTYPESEPARATOR, AC_TYPE_SEPARATOR, 0);

  updateColors();
}

//----------------------------------------------------------------------------------------------------------------------

CodeEditor::~CodeEditor() {
  base::NotificationCenter::get()->remove_observer(this);

  delete _find_panel;
  auto_completion_cancel();
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::updateColors() {
  bool darkMode = App::get()->isDarkModeActive();

  base::Color color = base::Color::getSystemColor(base::TextBackgroundColor);
  long mainBackground = color.toBGR();
  _code_editor_impl->send_editor(this, SCI_STYLESETBACK, STYLE_DEFAULT, mainBackground);

  // Propagate the new background color to all editor text styles.
  for (int i = 0; i < STYLE_DEFAULT; ++i)
    _code_editor_impl->send_editor(this, SCI_STYLESETBACK, i, mainBackground);
  for (int i = STYLE_LASTPREDEFINED + 1; i < STYLE_MAX; ++i)
    _code_editor_impl->send_editor(this, SCI_STYLESETBACK, i, mainBackground);

  for (int n = SC_MARKNUM_FOLDEREND; n <= SC_MARKNUM_FOLDEROPEN; ++n) {
    _code_editor_impl->send_editor(this, SCI_MARKERSETFORE, n, mainBackground); // Front/back meaning is reversed here.
    _code_editor_impl->send_editor(this, SCI_MARKERSETBACK, n, 0xA0B0B0);
  }

  _code_editor_impl->send_editor(this, SCI_STYLESETFORE, STYLE_LINENUMBER, darkMode ? 0x808080 : 0x907522);
  _code_editor_impl->send_editor(this, SCI_STYLESETBACK, STYLE_LINENUMBER, mainBackground);

  _code_editor_impl->send_editor(this, SCI_INDICSETFORE, ERROR_INDICATOR, darkMode ? 0x281FFF : 0x2119D0);

  // Other settings.
  color = Color::getSystemColor(base::SelectedTextBackgroundColor);
  _code_editor_impl->send_editor(this, SCI_SETSELBACK, 1, color.toBGR());
#ifndef __APPLE__
  _code_editor_impl->send_editor(this, SCI_SETSELALPHA, 64, 0);
#endif

  if (darkMode) {
    _code_editor_impl->send_editor(this, SCI_SETCARETLINEBACK, 0xFFFFFF, 0);
    _code_editor_impl->send_editor(this, SCI_SETCARETLINEBACKALPHA, 0x38, 0);
    _code_editor_impl->send_editor(this, SCI_SETCARETFORE, 0xAEAEAE, 0);
  } else {
    _code_editor_impl->send_editor(this, SCI_SETCARETLINEBACK, 0x000000, 0);
    _code_editor_impl->send_editor(this, SCI_SETCARETLINEBACKALPHA, 0x10, 0);
    _code_editor_impl->send_editor(this, SCI_SETCARETFORE, 0x404040, 0);
  }

  // - Call tips
  color = base::Color::getSystemColor(base::TextColor);
  _code_editor_impl->send_editor(this, SCI_CALLTIPSETFORE, color.toBGR(), 0);
  _code_editor_impl->send_editor(this, SCI_CALLTIPSETBACK, darkMode ? 0x404040 : 0xF8F8F8, 0);

  auto applyStyle = [&](int id, auto &values) {
    std::string value = darkMode ? values["fore-color-dark"] : values["fore-color-light"];
    if (value.empty())
      value = values["fore-color"]; // Backwards compatibility.

    if (!value.empty()) {
      Color color = Color::parse(value);
      _code_editor_impl->send_editor(this, SCI_STYLESETFORE, id, color.toBGR());
    }

    value = darkMode ? values["back-color-dark"] : values["back-color-light"];
    if (!value.empty()) {
      Color color = Color::parse(value);
      _code_editor_impl->send_editor(this, SCI_STYLESETBACK, id, color.toBGR());
    }

    value = base::tolower(values["bold"]);
    if (!value.empty()) {
      bool flag = value == "1" || value == "yes" || value == "true";
      _code_editor_impl->send_editor(this, SCI_STYLESETBOLD, id, flag);
    }

    value = base::tolower(values["italic"]);
    if (!value.empty()) {
      bool flag = value == "1" || value == "yes" || value == "true";
      _code_editor_impl->send_editor(this, SCI_STYLESETITALIC, id, flag);
    }
  };

  // First load the default style and apply that to all text style entries. Then loop again for individual settings.
  for (auto &style : _currentStyles) {
    if (style.first == 0) {
      for (int i = 0; i < STYLE_DEFAULT; ++i)
        applyStyle(i, style.second);
      for (int i = STYLE_LASTPREDEFINED + 1; i < STYLE_MAX; ++i)
        applyStyle(i, style.second);
      break;
    }
  }

  for (auto &style : _currentStyles) {
    if (style.first == 0)
      continue;
    applyStyle(style.first, style.second);
  }
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::setWidth(EditorMargin margin, int size, const std::string& adjustText) {
  if (!adjustText.empty())
    size = (int)_code_editor_impl->send_editor(this, SCI_TEXTWIDTH, STYLE_LINENUMBER, (sptr_t)adjustText.c_str());
  switch (margin) {
    case LineNumberMargin:
      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 0, size);
      break;
    case MarkersMargin:
      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 1, size);
      break;
    case FolderMargin:
      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 2, size);
      break;
    case TextMargin:
      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 3, size);
      break;
    default:
      break;
  }
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::setColor(EditorMargin margin, base::Color color, bool foreground) {
  switch (margin) {
    case FolderMargin:
    case LineNumberMargin:
    case TextMargin:
      if (foreground)
        _code_editor_impl->send_editor(this, SCI_STYLESETFORE, STYLE_LINENUMBER, color.toRGB());
      else
        _code_editor_impl->send_editor(this, SCI_STYLESETBACK, STYLE_LINENUMBER, color.toRGB());
      break;
    case MarkersMargin:
      for (int n = 25; n < 32; ++n) { // Markers 25..31 are reserved for folding.
        if (foreground)
          _code_editor_impl->send_editor(this, SCI_MARKERSETFORE, n, color.toRGB());
        else
          _code_editor_impl->send_editor(this, SCI_MARKERSETBACK, n, color.toRGB());
      }
      break;
    default:
      break;
  }
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::showMargin(EditorMargin margin, bool show) {
  sptr_t size = 0;
  const sptr_t defaultSize = 16;
  switch (margin) {
    case LineNumberMargin:
      if (!show)
        _marginSize.margin1 = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 0, 0);
      else {
        size = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 0, 0);
        if (size > 0)
          break;
        size = _marginSize.margin1 > 0 ? _marginSize.margin1 : defaultSize;
      }
      break;
    case MarkersMargin:
      if (!show)
        _marginSize.margin2 = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 1, 0);
      else {
        size = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 1, 0);
        if (size > 0)
          break;
        size = _marginSize.margin2 > 0 ? _marginSize.margin2 : defaultSize;
      }
      break;
    case FolderMargin:
      if (!show)
        _marginSize.margin3 = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 2, 0);
      else {
        size = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 2, 0);
        if (size > 0)
          break;
        size = _marginSize.margin3 > 0 ? _marginSize.margin3 : defaultSize;
      }
      break;
    case TextMargin:
      if (!show)
        _marginSize.margin4 = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 3, 0);
      else {
        size = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 3, 0);
        if (size > 0)
          break;
        size = _marginSize.margin4 > 0 ? _marginSize.margin4 : defaultSize;
      }
      break;
    default:
      break;
  }
  if (!show)
    setWidth(margin, 0);
  else
    setWidth(margin, (int)size);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::setMarginText(const std::string& str) {
  setMarginText(str, line_count() - 1);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::setScrollWidth(size_t width) {
  _code_editor_impl->send_editor(this, SCI_SETSCROLLWIDTH, width, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::setMarginText(const std::string& str, size_t line) {
  sptr_t size = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 3, 0);
  sptr_t lineNumberStyleWidth =
    _code_editor_impl->send_editor(this, SCI_TEXTWIDTH, STYLE_LINENUMBER, (sptr_t)str.c_str());
  if (lineNumberStyleWidth > size)
    _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 3, lineNumberStyleWidth);

  _code_editor_impl->send_editor(this, SCI_MARGINSETTEXT, line, (sptr_t)str.c_str());
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_text(const char* text) {
  _code_editor_impl->send_editor(this, SCI_SETTEXT, 0, (sptr_t)text);
}

//----------------------------------------------------------------------------------------------------------------------

int CodeEditor::getLineHeight(int line) {
  return (int)_code_editor_impl->send_editor(this, SCI_TEXTHEIGHT, line, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_value(const std::string& value) {
  // When passing text as std::string we have a length and can hence use a different
  // way to set the text in the control, which preserves embedded nulls.
  _code_editor_impl->send_editor(this, SCI_CLEARALL, 0, 0);
  _code_editor_impl->send_editor(this, SCI_APPENDTEXT, value.size(), (sptr_t)value.c_str());
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_text_keeping_state(const char* text) {
  sptr_t caret_position = _code_editor_impl->send_editor(this, SCI_GETCURRENTPOS, 0, 0);
  sptr_t selection_start = _code_editor_impl->send_editor(this, SCI_GETSELECTIONSTART, 0, 0);
  sptr_t selection_end = _code_editor_impl->send_editor(this, SCI_GETSELECTIONEND, 0, 0);
  sptr_t first_line = _code_editor_impl->send_editor(this, SCI_GETFIRSTVISIBLELINE, 0, 0);

  _code_editor_impl->send_editor(this, SCI_SETTEXT, 0, (sptr_t)text);

  _code_editor_impl->send_editor(this, SCI_SETCURRENTPOS, caret_position, 0);
  _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART, selection_start, 0);
  _code_editor_impl->send_editor(this, SCI_SETSELECTIONEND, selection_end, 0);
  _code_editor_impl->send_editor(this, SCI_SETFIRSTVISIBLELINE, first_line, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::append_text(const char* text, size_t length) {
  _code_editor_impl->send_editor(this, SCI_APPENDTEXT, length, (sptr_t)text);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::replace_selected_text(const std::string& text) {
  std::size_t start, length;
  get_selection(start, length);
  _code_editor_impl->send_editor(this, SCI_REPLACESEL, 0, (sptr_t)text.c_str());

  _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART, start + text.length(), 0);
  _code_editor_impl->send_editor(this, SCI_SETSELECTIONEND, start + text.length(), 0);
}

//----------------------------------------------------------------------------------------------------------------------

const std::string CodeEditor::get_text(bool selection_only) {
  char* text = nullptr;
  sptr_t length;
  if (selection_only) {
    length = _code_editor_impl->send_editor(this, SCI_GETSELTEXT, 0, 0);
    if (length > 0) {
      text = (char*)malloc(length);
    }
    if (text != nullptr) // Can be null if not memory is available.
      _code_editor_impl->send_editor(this, SCI_GETSELTEXT, length, (sptr_t)text);
  } else {
    length = _code_editor_impl->send_editor(this, SCI_GETTEXTLENGTH, 0, 0) + 1;
    if (length > 0) {
      text = (char*)malloc(length);
    }
    if (text != nullptr)
      _code_editor_impl->send_editor(this, SCI_GETTEXT, length, (sptr_t)text);
  }
  if (text != nullptr) {
    std::string result(text, length - 1);
    free(text);

    return result;
  }
  return "";
}

//----------------------------------------------------------------------------------------------------------------------

const std::string CodeEditor::get_text_in_range(size_t start, size_t end) {
  Sci_TextRange range;

  range.chrg.cpMin = (long)start;

  sptr_t length = _code_editor_impl->send_editor(this, SCI_GETTEXTLENGTH, 0, 0);
  range.chrg.cpMax = (long)(end > start + length ? length - start : end);

  range.lpstrText = (char*)malloc(end - start + 1); // Don't forget the 0 terminator.
  _code_editor_impl->send_editor(this, SCI_GETTEXTRANGE, 0, (sptr_t)&range);

  if (range.lpstrText == NULL)
    return std::string();

  std::string result(range.lpstrText, end - start);
  free(range.lpstrText);

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

std::pair<const char*, std::size_t> CodeEditor::get_text_ptr() {
  std::pair<const char*, std::size_t> result;
  result.first = reinterpret_cast<const char *>(_code_editor_impl->send_editor(this, SCI_GETCHARACTERPOINTER, 0, 0));
  result.second = _code_editor_impl->send_editor(this, SCI_GETTEXTLENGTH, 0, 0);

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_selection(std::size_t start, std::size_t length) {
  _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART, start, 0);
  _code_editor_impl->send_editor(this, SCI_SETSELECTIONEND, start + length, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::clear_selection() {
  sptr_t current_pos = _code_editor_impl->send_editor(this, SCI_GETCURRENTPOS, 0, 0);
  _code_editor_impl->send_editor(this, SCI_SETEMPTYSELECTION, current_pos, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::get_selection(std::size_t& start, std::size_t& length) {
  start = _code_editor_impl->send_editor(this, SCI_GETSELECTIONSTART, 0, 0);
  length = _code_editor_impl->send_editor(this, SCI_GETSELECTIONEND, 0, 0) - start;
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::get_range_of_line(ssize_t line, ssize_t& start, ssize_t& end) {
  start = _code_editor_impl->send_editor(this, SCI_POSITIONFROMLINE, line, 0);
  end = _code_editor_impl->send_editor(this, SCI_GETLINEENDPOSITION, line, 0);

  return start < 0 || end < 0;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::setupMarker(int marker, const std::string& name) {
  if (base::hasSuffix(name, ".xpm")) {
    std::string path = App::get()->get_resource_path(name);

    gchar* content = NULL;
    if (g_file_get_contents(path.c_str(), &content, NULL, NULL)) {
      _code_editor_impl->send_editor(this, SCI_MARKERDEFINEPIXMAP, marker, (sptr_t)content);
      g_free(content);
    }
  } else { // png
    if (ensureImage(name)) {
      auto &data = registeredImages[name];
      _code_editor_impl->send_editor(this, SCI_RGBAIMAGESETWIDTH, data.width, 0);
      _code_editor_impl->send_editor(this, SCI_RGBAIMAGESETHEIGHT, data.height, 0);
      _code_editor_impl->send_editor(this, SCI_RGBAIMAGESETSCALE, data.hiresImage ? 200 : 100, 0);
      _code_editor_impl->send_editor(this, SCI_MARKERDEFINERGBAIMAGE, marker, reinterpret_cast<sptr_t>(data.data));
    }
  }
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Called before lines are removed. Need to record disappearing markers.
 */
void CodeEditor::handleMarkerDeletion(size_t position, size_t length) {
  if (length == 0)
    return;

  LineMarkupChangeset changeset;
  if (length == static_cast<size_t>(_code_editor_impl->send_editor(this, SCI_GETLENGTH, 0, 0))) {
    // Clean editor? Send empty changeset to signal that all markers can go.
    _marker_changed_event(changeset, true);
    return;
  }

  sptr_t currentLine = _code_editor_impl->send_editor(this, SCI_LINEFROMPOSITION, position, 0);
  sptr_t endLine = _code_editor_impl->send_editor(this, SCI_LINEFROMPOSITION, position + length - 1, 0);

  // Don't include the first line as Scintilla merges current and last line markers, so we have to update them.
  ++currentLine;

  currentLine = _code_editor_impl->send_editor(this, SCI_MARKERNEXT, currentLine, LineMarkupAll);

  while (currentLine > -1 && currentLine <= endLine) {
    LineMarkup markup = (LineMarkup)_code_editor_impl->send_editor(this, SCI_MARKERGET, currentLine, LineMarkupAll);
    LineMarkupChangeEntry entry = {(int)currentLine, 0, markup};
    changeset.push_back(entry);

    // MARKERNEXT effectively searches lines for the given markers, the docs say.
    currentLine = _code_editor_impl->send_editor(this, SCI_MARKERNEXT, currentLine + 1, LineMarkupAll);
  }

  if (!changeset.empty())
    _marker_changed_event(changeset, true);
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Called after an edit action took place. We have to record markers that got moved by that action.
 */
void CodeEditor::handleMarkerMove(Sci_Position position, Sci_Position linesAdded) {
  if (linesAdded == 0)
    return;

  LineMarkupChangeset changeset;
  sptr_t currentLine = _code_editor_impl->send_editor(this, SCI_LINEFROMPOSITION, position, 0);

  if (linesAdded < 0) {
    // Lines have been deleted and markers merged. Since we don't know which have been combined remove them all
    // and send a separate deletion event for this case.
    _code_editor_impl->send_editor(this, SCI_MARKERDELETE, currentLine, -1);
    changeset.push_back({(int)currentLine, 0, LineMarkupAll});
    _marker_changed_event(changeset, true);
    changeset.clear();
  }

  // Ignore the first line if it has been edited after the line start.
  if (position > static_cast<Sci_Position>(_code_editor_impl->send_editor(this, SCI_POSITIONFROMLINE, currentLine, 0)))
    ++currentLine;

  currentLine = _code_editor_impl->send_editor(this, SCI_MARKERNEXT, currentLine, LineMarkupAll);
  while (currentLine > -1) {
    LineMarkup markup = (LineMarkup)_code_editor_impl->send_editor(this, SCI_MARKERGET, currentLine, LineMarkupAll);
    LineMarkupChangeEntry entry = {(int)(currentLine - linesAdded), (int)currentLine, markup};
    changeset.push_back(entry);

    currentLine = _code_editor_impl->send_editor(this, SCI_MARKERNEXT, currentLine + 1, LineMarkupAll);
  }

  if (!changeset.empty())
    _marker_changed_event(changeset, false);
}

//----------------------------------------------------------------------------------------------------------------------

char32_t CodeEditor::getCharAt(size_t position) {
  return static_cast<char32_t>(_code_editor_impl->send_editor(this, SCI_GETCHARAT, position, 0));
}

//----------------------------------------------------------------------------------------------------------------------

static const std::unordered_set<char32_t> braces = { '(', '{', '[', '<', ')', '}', ']', '>' };

void CodeEditor::updateBraceHighlighting() {
  size_t caretPos = get_caret_pos();
  ssize_t brace1Pos = INVALID_POSITION;
  ssize_t brace2Pos = INVALID_POSITION;

  // Highlight an opening brace regardless of the side the caret is, unless another opening brace follows directly
  // (and similary for closing braces).
  char32_t c = getCharAt(caretPos);

  if (braces.count(c) > 0)
    brace1Pos = caretPos;
  else if (caretPos > 0) {
    c = getCharAt(caretPos - 1);
    if (braces.count(c) > 0)
      brace1Pos = caretPos - 1;
  }

  if (brace1Pos > INVALID_POSITION) {
    brace2Pos = _code_editor_impl->send_editor(this, SCI_BRACEMATCH, brace1Pos, 0);
    if (brace2Pos == INVALID_POSITION)
      _code_editor_impl->send_editor(this, SCI_BRACEBADLIGHT, brace1Pos, 0);
    else
      _code_editor_impl->send_editor(this, SCI_BRACEHIGHLIGHT, brace1Pos, brace2Pos);
  } else
    _code_editor_impl->send_editor(this, SCI_BRACEHIGHLIGHT, brace1Pos, brace2Pos);
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Ensures the given image is loaded from disk and stored in our image list.
 * Returns true if the image exists.
 */
bool CodeEditor::ensureImage(std::string const& name) {
  if (registeredImages.find(name) != registeredImages.end())
    return true;

  cairo_surface_t* image = Utilities::load_icon(name);
  if (image == nullptr)
    return false;

  if (cairo_surface_status(image) != CAIRO_STATUS_SUCCESS) {
    cairo_surface_destroy(image);
    return false;
  }

  int width = cairo_image_surface_get_width(image);
  int height = cairo_image_surface_get_height(image);

  unsigned char* data = cairo_image_surface_get_data(image);

  // We not only need to keep the data around for the lifetime of the editor we also have
  // to swap blue and red.
  unsigned char *target = reinterpret_cast<unsigned char *>(malloc(4 * width * height));
  if (target != nullptr) {
    ImageRecord entry = {
      Utilities::is_hidpi_icon(image),
      cairo_image_surface_get_width(image),
      cairo_image_surface_get_height(image),
      target
    };
    registeredImages[name] = entry;
    int j = 0;
    while (j < 4 * width * height) {
      target[j] = data[j + 2];
      target[j + 1] = data[j + 1];
      target[j + 2] = data[j];
      target[j + 3] = data[j + 3];
      j += 4;
    }
  }
  cairo_surface_destroy(image);

  return true;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::loadConfiguration(SyntaxHighlighterLanguage language) {
  CodeEditorConfig config(language);

  // Keywords.
  if (language == LanguageMySQL56 || language == LanguageMySQL57 || language == LanguageMySQL80) {
    MySQLVersion version;
    switch (language) {
      case LanguageMySQL56:
        version = MySQLVersion::MySQL56;
        break;
      case LanguageMySQL57:
        version = MySQLVersion::MySQL57;
        break;
      default:
        version = MySQLVersion::MySQL80;
        break;
    }

    auto &functions = MySQLSymbolInfo::systemFunctionsForVersion(version);
    std::string list;
    for (auto const& name : functions)
      list += base::tolower(name) + " "; // Keyword matching in Scintilla uses lower case compare.

    _code_editor_impl->send_editor(this, SCI_SETKEYWORDS, 3, (sptr_t)list.c_str());

    auto &keywords = MySQLSymbolInfo::keywordsForVersion(version);
    list = "";
    for (auto const& name : keywords)
      list += base::tolower(name) + " ";

    _code_editor_impl->send_editor(this, SCI_SETKEYWORDS, 1, (sptr_t)list.c_str());

  } else {
    std::map<std::string, std::string> keywords = config.get_keywords();

    // Key word list sets are from currently active lexer, so that must be set before calling here.
    sptr_t length = _code_editor_impl->send_editor(this, SCI_DESCRIBEKEYWORDSETS, 0, 0);
    if (length > 0) {
      char* keyword_sets = (char*)malloc(length + 1);
      _code_editor_impl->send_editor(this, SCI_DESCRIBEKEYWORDSETS, 0, (sptr_t)keyword_sets);
      std::vector<std::string> keyword_list_names = base::split(keyword_sets, "\n");
      free(keyword_sets);

      for (auto iterator : keywords) {
        std::string list_name = iterator.first;
        int list_index = base::index_of(keyword_list_names, list_name);
        if (list_index > -1)
          _code_editor_impl->send_editor(this, SCI_SETKEYWORDS, list_index, reinterpret_cast<sptr_t>(iterator.second.c_str()));
      }
    }
  }

  // Properties.
  for (auto const& property : config.get_properties()) {
    _code_editor_impl->send_editor(this, SCI_SETPROPERTY,
      reinterpret_cast<uptr_t>(property.first.c_str()), reinterpret_cast<sptr_t>(property.second.c_str()));
  }

  // Settings.
  for (auto const& setting : config.get_settings()) {
    if (setting.first == "usetabs") {
      int value = base::atoi<int>(setting.second, 0);
      _code_editor_impl->send_editor(this, SCI_SETUSETABS, value, 0);
    } else if (setting.first == "tabwidth") {
      int value = base::atoi<int>(setting.second, 0);
      _code_editor_impl->send_editor(this, SCI_SETTABWIDTH, value, 0);
    } else if (setting.first == "indentation") {
      int value = base::atoi<int>(setting.second, 0);
      _code_editor_impl->send_editor(this, SCI_SETINDENT, value, 0);
    }
  }

  // Styles.
  _currentStyles = config.get_styles();
  updateColors();
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::handle_notification(const std::string &name, void *sender, NotificationInfo &info) {
  if (name == "GNColorsChanged")
    updateColors();
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_language(SyntaxHighlighterLanguage language) {
  switch (language) {
    case mforms::LanguageMySQL56:
    case mforms::LanguageMySQL57:
    case mforms::LanguageMySQL80:
      _code_editor_impl->send_editor(this, SCI_SETLEXER, SCLEX_MYSQL, 0);
      break;

    case mforms::LanguageHtml:
      _code_editor_impl->send_editor(this, SCI_SETLEXER, SCLEX_HTML, 0);
      break;

    case mforms::LanguagePython:
      _code_editor_impl->send_editor(this, SCI_SETLEXER, SCLEX_PYTHON, 0);
      break;

    case mforms::LanguageCpp:
    case mforms::LanguageJS:
    case mforms::LanguageJson:
      _code_editor_impl->send_editor(this, SCI_SETLEXER, SCLEX_CPP, 0);
      break;

    default:
      // No (known) language. Syntax highlighting will be switched off.
      _code_editor_impl->send_editor(this, SCI_SETLEXER, SCLEX_NULL, 0);
      return;
  }

  loadConfiguration(language);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::show_markup(LineMarkup markup, size_t line) {
  // The marker mask contains one bit for each set marker (0..31).
  sptr_t marker_mask = _code_editor_impl->send_editor(this, SCI_MARKERGET, line, 0);
  sptr_t new_marker_mask = 0;
  if ((markup & mforms::LineMarkupStatement) != 0) {
    if ((marker_mask & LineMarkupStatement) == 0)
      new_marker_mask |= LineMarkupStatement;
  }
  if ((markup & mforms::LineMarkupErrorContinue) != 0) {
    if ((marker_mask & LineMarkupErrorContinue) == 0)
      new_marker_mask |= LineMarkupErrorContinue;
  }
  if ((markup & mforms::LineMarkupError) != 0) {
    if ((marker_mask & LineMarkupError) == 0)
      new_marker_mask |= LineMarkupError;
  }
  if ((markup & mforms::LineMarkupBreakpoint) != 0) {
    if ((marker_mask & LineMarkupBreakpoint) == 0)
      new_marker_mask |= LineMarkupBreakpoint;
  }
  if ((markup & mforms::LineMarkupBreakpointHit) != 0) {
    if ((marker_mask & LineMarkupBreakpointHit) == 0)
      new_marker_mask |= LineMarkupBreakpointHit;
  }
  if ((markup & mforms::LineMarkupCurrent) != 0) {
    if ((marker_mask & LineMarkupCurrent) == 0)
      new_marker_mask |= LineMarkupCurrent;
  }

  if (new_marker_mask != 0)
    _code_editor_impl->send_editor(this, SCI_MARKERADDSET, line, new_marker_mask);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::remove_markup(LineMarkup markup, ssize_t line) {
  if (markup == mforms::LineMarkupAll || line < 0) {
    if (line < 0)
      _code_editor_impl->send_editor(this, SCI_MARKERDELETEALL, -1, 0);
    else
      _code_editor_impl->send_editor(this, SCI_MARKERDELETE, line, -1);
  } else {
    if ((markup & mforms::LineMarkupStatement) != 0)
      _code_editor_impl->send_editor(this, SCI_MARKERDELETE, line, CE_STATEMENT_MARKER);
    if ((markup & mforms::LineMarkupError) != 0)
      _code_editor_impl->send_editor(this, SCI_MARKERDELETE, line, CE_ERROR_MARKER);
    if ((markup & mforms::LineMarkupErrorContinue) != 0)
      _code_editor_impl->send_editor(this, SCI_MARKERDELETE, line, CE_ERROR_CONTINUE_MARKER);
    if ((markup & mforms::LineMarkupBreakpoint) != 0)
      _code_editor_impl->send_editor(this, SCI_MARKERDELETE, line, CE_BREAKPOINT_MARKER);
    if ((markup & mforms::LineMarkupBreakpointHit) != 0)
      _code_editor_impl->send_editor(this, SCI_MARKERDELETE, line, CE_BREAKPOINT_HIT_MARKER);
    if ((markup & mforms::LineMarkupCurrent) != 0)
      _code_editor_impl->send_editor(this, SCI_MARKERDELETE, line, CE_CURRENT_LINE_MARKER);
  }
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::has_markup(LineMarkup markup, size_t line) {
  sptr_t markers = _code_editor_impl->send_editor(this, SCI_MARKERGET, line, 0);

  if ((markup & markers) != 0)
    return true;

  return false;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::show_indicator(RangeIndicator indicator, size_t start, size_t length) {
  // Scintilla supports a model that not only sets an indicator in a given range but additionally
  // assigns a value to this indicator. This is to allow drawing an indicator with different styles.
  // However, currently all values are drawn in the same style and it is neither clear if that will ever
  // change nor what the actual use is, given that you always can set different indicators for different styles.
  // Unfortunately, you cannot query an indicator as such, only its value, so we have to set a value too
  // even if that is quite useless.
  switch (indicator) {
    case mforms::RangeIndicatorError:
      _code_editor_impl->send_editor(this, SCI_SETINDICATORCURRENT, ERROR_INDICATOR, 0);
      _code_editor_impl->send_editor(this, SCI_SETINDICATORVALUE, ERROR_INDICATOR_VALUE, 0);
      _code_editor_impl->send_editor(this, SCI_INDICATORFILLRANGE, start, length);

      break;
    case mforms::RangeIndicatorNone: // No effect here. Only used to have "none" value for indicator tests.
      break;
  }
}

//----------------------------------------------------------------------------------------------------------------------

RangeIndicator CodeEditor::indicator_at(size_t position) {
  sptr_t result = _code_editor_impl->send_editor(this, SCI_INDICATORVALUEAT, ERROR_INDICATOR, position);
  if (result == ERROR_INDICATOR_VALUE)
    return mforms::RangeIndicatorError;

  return mforms::RangeIndicatorNone;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::remove_indicator(RangeIndicator indicator, size_t start, size_t length) {
  switch (indicator) {
    case mforms::RangeIndicatorError:
      _code_editor_impl->send_editor(this, SCI_SETINDICATORCURRENT, ERROR_INDICATOR, 0);
      _code_editor_impl->send_editor(this, SCI_INDICATORCLEARRANGE, start, length);

      break;
    case mforms::RangeIndicatorNone: // No effect here. Only needed to have a "none" value for indicator tests.
      break;
  }
}

//----------------------------------------------------------------------------------------------------------------------

size_t mforms::CodeEditor::line_count() {
  return _code_editor_impl->send_editor(this, SCI_GETLINECOUNT, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

size_t CodeEditor::text_length() {
  return _code_editor_impl->send_editor(this, SCI_GETLENGTH, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

size_t CodeEditor::position_from_line(size_t line_number) {
  return _code_editor_impl->send_editor(this, SCI_POSITIONFROMLINE, line_number, 0);
}

//----------------------------------------------------------------------------------------------------------------------

size_t CodeEditor::line_from_position(size_t position) {
  return _code_editor_impl->send_editor(this, SCI_LINEFROMPOSITION, position, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_font(const std::string& fontDescription) {
  // Set this font for all styles.
  std::string font;
  float size;
  bool bold;
  bool italic;

  // Bold and italic are ignored as they can be set by a highlighter style.
  if (base::parse_font_description(fontDescription, font, size, bold, italic)) {
#if !defined(_MSC_VER) && !defined(__APPLE__)
    // Scintilla requires the ! in front of the font name to interpret it as a pango/fontconfig font.
    if (!font.empty() && font[0] != '!')
      font = "!" + font;
#endif

    for (int i = 0; i < 128; i++) {
      _code_editor_impl->send_editor(this, SCI_STYLESETFONT, i, (sptr_t)font.c_str());
      _code_editor_impl->send_editor(this, SCI_STYLESETSIZE, i, (sptr_t)size);
    }
  }

  // Recompute the line number margin width if it is visible.
  sptr_t lineNumberStyleWidth = _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 0, 0);
  if (lineNumberStyleWidth > 0) {
    lineNumberStyleWidth = _code_editor_impl->send_editor(this, SCI_TEXTWIDTH, STYLE_LINENUMBER, (sptr_t) "_9999");
    _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 0, lineNumberStyleWidth);
  }
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Converts scintilla key modifier codes to mforms codes.
 */
mforms::ModifierKey getModifiers(int scintilla_modifiers) {
  mforms::ModifierKey modifiers = mforms::ModifierNoModifier;
  if ((scintilla_modifiers & SCMOD_CTRL) == SCMOD_CTRL)
    modifiers = modifiers | mforms::ModifierControl;
  if ((scintilla_modifiers & SCMOD_ALT) == SCMOD_ALT)
    modifiers = modifiers | mforms::ModifierAlt;
  if ((scintilla_modifiers & SCMOD_SHIFT) == SCMOD_SHIFT)
    modifiers = modifiers | mforms::ModifierShift;

  return modifiers;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::on_notify(SCNotification* notification) {
  switch (notification->nmhdr.code) {
    case SCN_MARGINCLICK: {
      sptr_t line = _code_editor_impl->send_editor(this, SCI_LINEFROMPOSITION, notification->position, 0);

#ifndef __APPLE__
      // This handling is already implemented in ScintillaView.mm.
      if (notification->margin == 2) {
        // A click on the folder margin. Toggle the current line if possible.
        _code_editor_impl->send_editor(this, SCI_TOGGLEFOLD, line, 0);
      }
#endif

      mforms::ModifierKey modifiers = getModifiers(notification->modifiers);
      _gutter_clicked_event(notification->margin, (int)line, modifiers);
      break;
    }

    case SCN_MODIFIED: {
      // Check for markers that would get removed.
      // Usually doesn't come with any of the other notification types.
      if ((notification->modificationType & SC_MOD_BEFOREDELETE) != 0)
        handleMarkerDeletion(notification->position, notification->length);

      // Text insertion or removal.
      if ((notification->modificationType & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT)) != 0) {
        handleMarkerMove(notification->position, notification->linesAdded);

        _change_event(notification->position, notification->length, notification->linesAdded,
                      (notification->modificationType & SC_MOD_INSERTTEXT) != 0);
      }

      break;
    }

    case SCN_AUTOCSELECTION:
      _auto_completion_event(mforms::AutoCompletionSelection, notification->position, notification->text);
      break;

    case SCN_AUTOCCANCELLED:
      _auto_completion_event(mforms::AutoCompletionCancelled, 0, "");
      break;

    case SCN_AUTOCCHARDELETED:
      _auto_completion_event(mforms::AutoCompletionCharDeleted, 0, "");
      break;

    case SCN_DWELLSTART:
      _dwell_event(true, notification->position, notification->x, notification->y);
      break;

    case SCN_DWELLEND:
      _dwell_event(false, 0, 0, 0);
      break;

    case SCN_UPDATEUI:
      switch (notification->updated) {
        case SC_UPDATE_CONTENT: // Contents, styling or markers have been changed.
          break;
        case SC_UPDATE_SELECTION: // Selection has been changed or the caret moved.
          updateBraceHighlighting();
          NotificationCenter::get()->send("GNTextSelectionChanged", this);
          break;
        case SC_UPDATE_V_SCROLL: // Scrolled vertically.
          break;
        case SC_UPDATE_H_SCROLL: // Scrolled horizontally.
          break;
      }
      break;

    case SCN_CHARADDED:
      _char_added_event(notification->ch);
      if (_auto_indent && notification->ch == '\n') {
        sptr_t pos = _code_editor_impl->send_editor(this, SCI_GETCURRENTPOS, 0, 0);
        sptr_t line = _code_editor_impl->send_editor(this, SCI_LINEFROMPOSITION, pos, 0);
        if (line > 0) {
          sptr_t indentation = _code_editor_impl->send_editor(this, SCI_GETLINEINDENTATION, line - 1, 0);
          if (indentation > 0) {
            // Switch off tabs for a moment. We don't want a mix of tabs and spaces auto inserted
            // and tabs mess up the new indentation.
            sptr_t use_tabs = _code_editor_impl->send_editor(this, SCI_GETUSETABS, 0, 0);
            _code_editor_impl->send_editor(this, SCI_SETUSETABS, 0, 0);

            _code_editor_impl->send_editor(this, SCI_SETLINEINDENTATION, line, indentation);
            _code_editor_impl->send_editor(this, SCI_GOTOPOS, pos + indentation, 0);

            _code_editor_impl->send_editor(this, SCI_SETUSETABS, use_tabs, 0);
          }
        }
      }
      break;

    case SCN_FOCUSIN:
      focus_changed();
      break;

    case SCN_FOCUSOUT:
      _signal_lost_focus(); // For validation, parsing etc.
      break;
  }
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::on_command(int command) {
  // TODO: removal candidate.
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::key_event(KeyCode code, ModifierKey modifier, const std::string& text) {
  // Return true if the key event can be further processed by the sender.
  // Return false if it is handled in backend code.
  if (_key_event_signal.empty())
    return true;

  return *_key_event_signal(code, modifier, text);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_features(CodeEditorFeature features, bool flag) {
  if ((features & mforms::FeatureWrapText) != 0) {
    if (flag)
      _code_editor_impl->send_editor(this, SCI_SETWRAPMODE, SC_WRAP_WORD, 0);
    else
      _code_editor_impl->send_editor(this, SCI_SETWRAPMODE, SC_WRAP_NONE, 0);
  }

  if ((features & mforms::FeatureGutter) != 0) {
    if (flag) {
      sptr_t width = _code_editor_impl->send_editor(this, SCI_TEXTWIDTH, STYLE_LINENUMBER, (sptr_t) "_9999");

      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 0, width); // line numbers
      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 1, 16);    // markers
      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 2, 16);    // fold markers
    } else {
      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 0, 0);
      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 1, 0);
      _code_editor_impl->send_editor(this, SCI_SETMARGINWIDTHN, 2, 0);
    }
  }

  if ((features & mforms::FeatureReadOnly) != 0)
    _code_editor_impl->send_editor(this, SCI_SETREADONLY, flag, 0);

  if ((features & mforms::FeatureShowSpecial) != 0) {
    _code_editor_impl->send_editor(this, SCI_SETVIEWEOL, flag, 0);
    if (flag)
      _code_editor_impl->send_editor(this, SCI_SETVIEWWS, SCWS_VISIBLEALWAYS, 0);
    else
      _code_editor_impl->send_editor(this, SCI_SETVIEWWS, SCWS_INVISIBLE, 0);
  }

  if ((features & mforms::FeatureUsePopup) != 0)
    _code_editor_impl->send_editor(this, SCI_USEPOPUP, flag, 0);

  if ((features & mforms::FeatureConvertEolOnPaste) != 0)
    _code_editor_impl->send_editor(this, SCI_SETPASTECONVERTENDINGS, flag, 0);

  if ((features & mforms::FeatureScrollOnResize) != 0)
    _scroll_on_resize = true;

  if ((features & mforms::FeatureFolding) != 0)
    _code_editor_impl->send_editor(this, SCI_SETPROPERTY, (uptr_t) "fold", flag ? (sptr_t) "1" : (sptr_t) "0");

  if ((features & mforms::FeatureAutoIndent) != 0)
    _auto_indent = true;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::toggle_features(CodeEditorFeature features) {
  // Toggling a feature involves querying its current state which is sometimes not possible with a
  // single value, so instead of returning the current state and let the application call
  // set_features we do it internally with this toggle_features function.

  if ((features & mforms::FeatureWrapText) != 0) {
    if (_code_editor_impl->send_editor(this, SCI_GETWRAPMODE, 0, 0) == SC_WRAP_NONE)
      _code_editor_impl->send_editor(this, SCI_SETWRAPMODE, SC_WRAP_WORD, 0);
    else
      _code_editor_impl->send_editor(this, SCI_SETWRAPMODE, SC_WRAP_NONE, 0);
  }

  if ((features & mforms::FeatureGutter) != 0)
    set_features(mforms::FeatureGutter, _code_editor_impl->send_editor(this, SCI_GETMARGINWIDTHN, 0, 0) == 0);

  if ((features & mforms::FeatureReadOnly) != 0)
    set_features(mforms::FeatureReadOnly, _code_editor_impl->send_editor(this, SCI_GETREADONLY, 0, 0) == 0);

  if ((features & mforms::FeatureShowSpecial) != 0)
    set_features(mforms::FeatureShowSpecial, _code_editor_impl->send_editor(this, SCI_GETVIEWEOL, 0, 0) == 0);

  // if ((features & mforms::FeatureUsePopup) != 0)
  // Not searchable so we cannot toggle it.

  if ((features & mforms::FeatureConvertEolOnPaste) != 0)
    set_features(mforms::FeatureConvertEolOnPaste,
                 _code_editor_impl->send_editor(this, SCI_GETPASTECONVERTENDINGS, 0, 0) != 0);

  if ((features & mforms::FeatureScrollOnResize) != 0)
    _scroll_on_resize = !_scroll_on_resize;

  if ((features & mforms::FeatureFolding) != 0) {
    bool folding_enabled = _code_editor_impl->send_editor(this, SCI_GETPROPERTYINT, (uptr_t) "fold", 0) != 0;
    _code_editor_impl->send_editor(this, SCI_SETPROPERTY, (uptr_t) "fold",
                                   folding_enabled ? (sptr_t) "0" : (sptr_t) "1");
  }

  if ((features & mforms::FeatureAutoIndent) != 0)
    _auto_indent = !_auto_indent;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_read_only(bool flag) {
  _code_editor_impl->send_editor(this, SCI_SETREADONLY, flag, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::reset_undo_stack() {
  _code_editor_impl->send_editor(this, SCI_EMPTYUNDOBUFFER, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::reset_dirty() {
  _code_editor_impl->send_editor(this, SCI_SETSAVEPOINT, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::is_dirty() {
  return _code_editor_impl->send_editor(this, SCI_GETMODIFY, 0, 0) != 0;
}

//----------------------------------------------------------------------------------------------------------------------

size_t CodeEditor::get_caret_pos() {
  return _code_editor_impl->send_editor(this, SCI_GETCURRENTPOS, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_caret_pos(size_t position) {
  _code_editor_impl->send_editor(this, SCI_GOTOPOS, position, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::get_line_column_pos(size_t position, size_t& line, size_t& column) {
  line = _code_editor_impl->send_editor(this, SCI_LINEFROMPOSITION, position, 0);
  column = _code_editor_impl->send_editor(this, SCI_GETCOLUMN, position, 0);
  ;
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::can_undo() {
  return _code_editor_impl->send_editor(this, SCI_CANUNDO, 0, 0) != 0;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::undo() {
  _code_editor_impl->send_editor(this, SCI_UNDO, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::can_redo() {
  return _code_editor_impl->send_editor(this, SCI_CANREDO, 0, 0) != 0;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::redo() {
  _code_editor_impl->send_editor(this, SCI_REDO, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::can_cut() {
  return can_copy() && can_delete();
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::cut() {
  _code_editor_impl->send_editor(this, SCI_CUT, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::can_copy() {
  sptr_t length = _code_editor_impl->send_editor(this, SCI_GETSELECTIONEND, 0, 0) -
                  _code_editor_impl->send_editor(this, SCI_GETSELECTIONSTART, 0, 0);
  return length > 0;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::copy() {
  _code_editor_impl->send_editor(this, SCI_COPY, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::can_paste() {
  return _code_editor_impl->send_editor(this, SCI_CANPASTE, 0, 0) != 0;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::paste() {
  _code_editor_impl->send_editor(this, SCI_PASTE, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::can_delete() {
  return can_copy() && _code_editor_impl->send_editor(this, SCI_GETREADONLY, 0, 0) == 0;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::do_delete() {
  replace_selected_text("");
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::select_all() {
  _code_editor_impl->send_editor(this, SCI_SELECTALL, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_status_text(const std::string& text) {
  // Optional implementation.
  if (_code_editor_impl->set_status_text != NULL)
    _code_editor_impl->set_status_text(this, text);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::show_find_panel(bool replace) {
  if (_find_panel == NULL)
    _find_panel = new FindPanel(this);
  _find_panel->enable_replace(replace);

  if (_show_find_panel)
    _show_find_panel(this, true);
  _find_panel->focus();
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::hide_find_panel() {
  if (_find_panel == NULL)
    return;

  if (_show_find_panel && _find_panel->is_shown())
    _show_find_panel(this, false);
  focus();
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_show_find_panel_callback(std::function<void(CodeEditor*, bool)> callback) {
  _show_find_panel = callback;
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::find_and_highlight_text(const std::string& search_text, FindFlags flags, bool scroll_to,
                                         bool backwards) {
  if (search_text.size() == 0)
    return false;

  bool wrap = (flags & FindWrapAround) != 0;
  int search_flags = 0;
  if (flags & FindMatchCase)
    search_flags |= SCFIND_MATCHCASE;
  if (flags & FindWholeWords)
    search_flags |= SCFIND_WHOLEWORD;
  if (flags & FindRegex)
    search_flags |= SCFIND_REGEXP;

  sptr_t selection_start = _code_editor_impl->send_editor(this, SCI_GETSELECTIONSTART, 0, 0);
  sptr_t selection_end = _code_editor_impl->send_editor(this, SCI_GETSELECTIONEND, 0, 0);

  // Sets the start point for the upcoming search to the begin of the current selection.
  // For forward searches we have therefore to set the selection start to the current selection end
  // for proper incremental search. This does not harm as we either get a new selection if something
  // is found or the previous selection is restored.
  if (!backwards)
    _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART, selection_end, 0);
  _code_editor_impl->send_editor(this, SCI_SEARCHANCHOR, 0, 0);

  sptr_t result = 0;

  // The following call will also set the selection if something was found.
  if (backwards) {
    result = _code_editor_impl->send_editor(this, SCI_SEARCHPREV, search_flags, (sptr_t)search_text.c_str());
    if (result < 0 && wrap) {
      // Try again from the end of the document if nothing could be found so far and
      // wrapped search is set.
      _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART,
                                     _code_editor_impl->send_editor(this, SCI_GETTEXTLENGTH, 0, 0), 0);
      _code_editor_impl->send_editor(this, SCI_SEARCHANCHOR, 0, 0);
      result = _code_editor_impl->send_editor(this, SCI_SEARCHNEXT, search_flags, (sptr_t)search_text.c_str());
    }
  } else {
    result = _code_editor_impl->send_editor(this, SCI_SEARCHNEXT, search_flags, (sptr_t)search_text.c_str());
    if (result < 0 && wrap) {
      // Try again from the start of the document if nothing could be found so far and
      // wrapped search is set.
      _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART, 0, 0);
      _code_editor_impl->send_editor(this, SCI_SEARCHANCHOR, 0, 0);
      result = _code_editor_impl->send_editor(this, SCI_SEARCHNEXT, search_flags, (sptr_t)search_text.c_str());
    }
  }

  if (result >= 0) {
    if (scroll_to)
      _code_editor_impl->send_editor(this, SCI_SCROLLCARET, 0, 0);
  } else {
    // Restore the former selection if we did not find anything.
    _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART, selection_start, 0);
    _code_editor_impl->send_editor(this, SCI_SETSELECTIONEND, selection_end, 0);
  }
  return (result >= 0) ? true : false;
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Searches the given text and replaces it by new_text. Returns the number of replacements performed.
 */
size_t CodeEditor::find_and_replace_text(const std::string& search_text, const std::string& new_text, FindFlags flags,
                                         bool do_all) {
  if (search_text.size() == 0)
    return 0;

  // The current position is where we start searching for single occurrences. Otherwise we start at
  // the beginning of the document.
  sptr_t start_position;
  if (do_all)
    start_position = 0;
  else
    start_position = _code_editor_impl->send_editor(this, SCI_GETCURRENTPOS, 0, 0);
  sptr_t endPosition = _code_editor_impl->send_editor(this, SCI_GETTEXTLENGTH, 0, 0);

  int search_flags = 0;
  if (flags & FindMatchCase)
    search_flags |= SCFIND_MATCHCASE;
  if (flags & FindWholeWords)
    search_flags |= SCFIND_WHOLEWORD;
  if (flags & FindRegex)
    search_flags |= SCFIND_REGEXP;
  _code_editor_impl->send_editor(this, SCI_SETSEARCHFLAGS, search_flags, 0);
  _code_editor_impl->send_editor(this, SCI_SETTARGETSTART, start_position, 0);
  _code_editor_impl->send_editor(this, SCI_SETTARGETEND, endPosition, 0);

  sptr_t result;

  size_t replace_count = 0;
  if (do_all) {
    while (true) {
      result =
        _code_editor_impl->send_editor(this, SCI_SEARCHINTARGET, search_text.size(), (sptr_t)search_text.c_str());
      if (result < 0)
        break;

      replace_count++;
      /*result =*/_code_editor_impl->send_editor(this, SCI_REPLACETARGET, new_text.size(), (sptr_t)new_text.c_str());

      // The replacement changes the target range to the replaced text. Continue after that until the end.
      // The text length might be changed by the replacement so make sure the target end is the actual
      // text end.
      _code_editor_impl->send_editor(this, SCI_SETTARGETSTART,
                                     _code_editor_impl->send_editor(this, SCI_GETTARGETEND, 0, 0), 0);
      _code_editor_impl->send_editor(this, SCI_SETTARGETEND,
                                     _code_editor_impl->send_editor(this, SCI_GETTEXTLENGTH, 0, 0), 0);
    }
  } else {
    result = _code_editor_impl->send_editor(this, SCI_SEARCHINTARGET, search_text.size(), (sptr_t)search_text.c_str());
    replace_count = (result < 0) ? 0 : 1;

    if (replace_count > 0) {
      /*result =*/_code_editor_impl->send_editor(this, SCI_REPLACETARGET, new_text.size(), (sptr_t)new_text.c_str());

      // For a single replace we set the new selection to the replaced text.
      _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART,
                                     _code_editor_impl->send_editor(this, SCI_GETTARGETSTART, 0, 0), 0);
      _code_editor_impl->send_editor(this, SCI_SETSELECTIONEND,
                                     _code_editor_impl->send_editor(this, SCI_GETTARGETEND, 0, 0), 0);
    }
  }

  return replace_count;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::jump_to_next_placeholder() {
  sptr_t current_pos = _code_editor_impl->send_editor(this, SCI_GETCURRENTPOS, 0, 0);
  sptr_t text_size = _code_editor_impl->send_editor(this, SCI_GETLENGTH, 0, 0);
  Sci_TextToFind what;

  what.lpstrText = (char*)"<{";
  what.chrg.cpMin = (long)current_pos;
  what.chrg.cpMax = (long)text_size;
  sptr_t result = _code_editor_impl->send_editor(this, SCI_FINDTEXT, 0, (sptr_t)&what);
  bool found = false;
  if (result >= 0) {
    static const int max_placeholder_length = 256;
    what.lpstrText = (char*)"}>";
    what.chrg.cpMin = (long)result;
    what.chrg.cpMax = what.chrg.cpMin + max_placeholder_length; // arbitrary max size for placeholder text
    result = _code_editor_impl->send_editor(this, SCI_FINDTEXT, 0, (sptr_t)&what);
    if (result >= 0) {
      char buffer[max_placeholder_length];
      Sci_TextRange tr;
      tr.chrg.cpMin = what.chrg.cpMin;
      tr.chrg.cpMax = (long)result + 2;
      tr.lpstrText = buffer;
      _code_editor_impl->send_editor(this, SCI_GETTEXTRANGE, 0, (sptr_t)&tr);

      if (!memchr(buffer, '\n', tr.chrg.cpMax - tr.chrg.cpMin)) {
        // jump to placeholder and select it
        _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART, tr.chrg.cpMin, 0);
        _code_editor_impl->send_editor(this, SCI_SETSELECTIONEND, tr.chrg.cpMax, 0);
        _code_editor_impl->send_editor(this, SCI_SCROLLCARET, 0, 0);
        found = true;
      }
    }
  }
  if (!found) {
    // restore the cursor to where it was
    _code_editor_impl->send_editor(this, SCI_SETSELECTIONSTART, current_pos, 0);
    _code_editor_impl->send_editor(this, SCI_SETSELECTIONEND, current_pos, 0);
  }
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::auto_completion_show(size_t chars_entered, const std::vector<std::pair<int, std::string> >& entries) {
  if (entries.size() == 0)
    return;

  std::stringstream list;
  for (size_t i = 0; i < entries.size(); ++i) {
    if (i > 0)
      list << AC_LIST_SEPARATOR;
    list << entries[i].second;
    if (entries[i].first > -1)
      list << AC_TYPE_SEPARATOR << entries[i].first;
  }
  _code_editor_impl->send_editor(this, SCI_AUTOCSHOW, chars_entered, (sptr_t)list.str().c_str());
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::auto_completion_show(size_t chars_entered, const std::vector<std::string>& entries) {
  std::stringstream list;
  for (size_t i = 0; i < entries.size(); ++i) {
    if (i > 0)
      list << AC_LIST_SEPARATOR;
    list << entries[i];
  }
  _code_editor_impl->send_editor(this, SCI_AUTOCSHOW, chars_entered, (sptr_t)list.str().c_str());
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::auto_completion_cancel() {
  _code_editor_impl->send_editor(this, SCI_AUTOCCANCEL, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::auto_completion_options(bool ignore_case, bool choose_single, bool auto_hide, bool drop_rest_of_word,
                                         bool cancel_at_start) {
  _code_editor_impl->send_editor(this, SCI_AUTOCSETIGNORECASE, ignore_case, 0);
  _code_editor_impl->send_editor(this, SCI_AUTOCSETCHOOSESINGLE, choose_single, 0);
  _code_editor_impl->send_editor(this, SCI_AUTOCSETAUTOHIDE, auto_hide, 0);
  _code_editor_impl->send_editor(this, SCI_AUTOCSETDROPRESTOFWORD, drop_rest_of_word, 0);
  _code_editor_impl->send_editor(this, SCI_AUTOCSETCANCELATSTART, cancel_at_start, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::auto_completion_max_size(int width, int height) {
  _code_editor_impl->send_editor(this, SCI_AUTOCSETMAXHEIGHT, height, 0);
  _code_editor_impl->send_editor(this, SCI_AUTOCSETMAXWIDTH, width, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::auto_completion_register_images(const std::vector<std::pair<int, std::string> > &images) {
  for (auto &image : images) {
    if (ensureImage(image.second)) {
      auto &data = registeredImages[image.second];
      _code_editor_impl->send_editor(this, SCI_RGBAIMAGESETWIDTH, data.width, 0);
      _code_editor_impl->send_editor(this, SCI_RGBAIMAGESETHEIGHT, data.height, 0);

      // Scale factors are not supported for code completion images.
      //_code_editor_impl->send_editor(this, SCI_RGBAIMAGESETSCALE, data.hiresImage ? 200 : 100, 0);

      _code_editor_impl->send_editor(this, SCI_REGISTERRGBAIMAGE, image.first, reinterpret_cast<sptr_t>(data.data));
    }
  }
}

//----------------------------------------------------------------------------------------------------------------------

bool CodeEditor::auto_completion_active() {
  return _code_editor_impl->send_editor(this, SCI_AUTOCACTIVE, 0, 0) != 0;
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::auto_completion_stops(const std::string& stops) {
  _code_editor_impl->send_editor(this, SCI_AUTOCSTOPS, 0, (sptr_t)stops.c_str());
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::auto_completion_fillups(const std::string& fillups) {
  _code_editor_impl->send_editor(this, SCI_AUTOCSETFILLUPS, 0, (sptr_t)fillups.c_str());
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::show_calltip(bool show, size_t position, const std::string& value) {
  if (show)
    _code_editor_impl->send_editor(this, SCI_CALLTIPSHOW, position, (sptr_t)value.c_str());
  else
    _code_editor_impl->send_editor(this, SCI_CALLTIPCANCEL, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::set_eol_mode(mforms::EndOfLineMode mode, bool convert) {
  _code_editor_impl->send_editor(this, SCI_SETEOLMODE, mode, 0);
  if (convert)
    _code_editor_impl->send_editor(this, SCI_CONVERTEOLS, mode, 0);
}

//----------------------------------------------------------------------------------------------------------------------

sptr_t CodeEditor::send_editor(unsigned int message, uptr_t wParam, sptr_t lParam) {
  return _code_editor_impl->send_editor(this, message, wParam, lParam);
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::lost_focus() {
  _signal_lost_focus();
}

//----------------------------------------------------------------------------------------------------------------------

void CodeEditor::resize() {
  if (_scroll_on_resize)
    _code_editor_impl->send_editor(this, SCI_SCROLLCARET, 0, 0);
}

//----------------------------------------------------------------------------------------------------------------------
