Fix file permissions
This commit is contained in:
Executable → Regular
+4
-1
@@ -17,7 +17,10 @@ void Editor::ensure_cursor() {
|
||||
render_height < this->size.row) {
|
||||
render_height++;
|
||||
last_visible = vline.first;
|
||||
if (cursor >= vline.first && cursor < vline.second)
|
||||
log("Render H: %d, Cursor: %d,%d, Visible: %d,%d - %d,%d", render_height,
|
||||
this->cursor.row, this->cursor.col, vline.first.row, vline.first.col,
|
||||
vline.second.row, vline.second.col);
|
||||
if (cursor >= vline.first && cursor <= vline.second)
|
||||
return;
|
||||
}
|
||||
cursor = last_visible;
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+456
-458
@@ -1,458 +1,456 @@
|
||||
// #include "editor/decl.h"
|
||||
// #include "editor/editor.h"
|
||||
// #include "io/knot.h"
|
||||
// #include "io/sysio.h"
|
||||
// #include "lsp/lsp.h"
|
||||
// #include "main.h"
|
||||
// #include "ui/completionbox.h"
|
||||
// #include "utils/utils.h"
|
||||
//
|
||||
// inline static std::string completion_prefix(Editor *editor) {
|
||||
// Coord hook = editor->completion.hook;
|
||||
// Coord cur = editor->cursor;
|
||||
// if (hook.row != cur.row || cur.col < hook.col)
|
||||
// return "";
|
||||
// LineIterator *it = begin_l_iter(editor->root, hook.row);
|
||||
// uint32_t line_len;
|
||||
// char *line = next_line(it, &line_len);
|
||||
// if (!line) {
|
||||
// free(it->buffer);
|
||||
// free(it);
|
||||
// return "";
|
||||
// }
|
||||
// std::string prefix(line + hook.col, cur.col - hook.col);
|
||||
// free(it->buffer);
|
||||
// free(it);
|
||||
// return prefix;
|
||||
// }
|
||||
//
|
||||
// inline static void completion_adjust_scroll(CompletionSession &s) {
|
||||
// if (s.visible.empty())
|
||||
// return;
|
||||
// int vi = -1;
|
||||
// for (size_t i = 0; i < s.visible.size(); i++)
|
||||
// if (s.visible[i] == s.select) {
|
||||
// vi = (int)i;
|
||||
// break;
|
||||
// }
|
||||
// if (vi < 0)
|
||||
// return;
|
||||
// if ((uint32_t)vi < s.scroll)
|
||||
// s.scroll = vi;
|
||||
// else if ((uint32_t)vi >= s.scroll + 8)
|
||||
// s.scroll = vi - 7;
|
||||
// }
|
||||
//
|
||||
// void completion_filter(Editor *editor) {
|
||||
// auto &session = editor->completion;
|
||||
// std::string prefix = completion_prefix(editor);
|
||||
// session.visible.clear();
|
||||
// for (size_t i = 0; i < session.items.size(); ++i) {
|
||||
// const auto &item = session.items[i];
|
||||
// const std::string &key = item.filter;
|
||||
// if (key.size() >= prefix.size() &&
|
||||
// key.compare(0, prefix.size(), prefix) == 0)
|
||||
// session.visible.push_back(i);
|
||||
// }
|
||||
// if (session.visible.empty()) {
|
||||
// session.box.hidden = true;
|
||||
// return;
|
||||
// }
|
||||
// if (std::find(session.visible.begin(), session.visible.end(),
|
||||
// session.select) == session.visible.end())
|
||||
// session.select = session.visible[0];
|
||||
// session.box.hidden = false;
|
||||
// session.scroll = 0;
|
||||
// completion_adjust_scroll(session);
|
||||
// session.box.render_update();
|
||||
// }
|
||||
//
|
||||
// void completion_request(Editor *editor) {
|
||||
// Coord hook = editor->cursor;
|
||||
// editor->word_boundaries(editor->cursor, &hook.col, nullptr, nullptr,
|
||||
// nullptr); editor->completion.hook = hook; editor->completion.complete =
|
||||
// false; editor->completion.active = false; editor->completion.items.clear();
|
||||
// editor->completion.visible.clear();
|
||||
// editor->completion.select = 0;
|
||||
// editor->completion.version = editor->lsp_version;
|
||||
// LSPPending *pending = new LSPPending();
|
||||
// pending->editor = editor;
|
||||
// pending->callback = [](Editor *editor, const json &message) {
|
||||
// auto &session = editor->completion;
|
||||
// std::unique_lock lock(session.mtx);
|
||||
// std::vector<json> items_json;
|
||||
// std::vector<char> end_chars_def;
|
||||
// int insert_text_format = 1;
|
||||
// int insert_text_mode = 1;
|
||||
// if (message.contains("result")) {
|
||||
// auto &result = message["result"];
|
||||
// if (result.is_array()) {
|
||||
// items_json = result.get<std::vector<json>>();
|
||||
// session.complete = true;
|
||||
// if (items_json.empty())
|
||||
// return;
|
||||
// editor->completion.active = true;
|
||||
// } else if (result.is_object() && result.contains("items")) {
|
||||
// auto &list = result;
|
||||
// items_json = list["items"].get<std::vector<json>>();
|
||||
// if (items_json.empty())
|
||||
// return;
|
||||
// editor->completion.active = true;
|
||||
// session.complete = !list.value("isIncomplete", false);
|
||||
// if (list.contains("itemDefaults") &&
|
||||
// list["itemDefaults"].is_object()) {
|
||||
// auto &defs = list["itemDefaults"];
|
||||
// if (defs.contains("insertTextFormat") &&
|
||||
// defs["insertTextFormat"].is_number())
|
||||
// insert_text_format = defs["insertTextFormat"].get<int>();
|
||||
// if (defs.contains("insertTextMode") &&
|
||||
// defs["insertTextMode"].is_number())
|
||||
// insert_text_mode = defs["insertTextMode"].get<int>();
|
||||
// if (defs.contains("textEdit"))
|
||||
// if (defs["textEdit"].is_array())
|
||||
// for (auto &c : defs["textEdit"]) {
|
||||
// if (!c.is_string())
|
||||
// continue;
|
||||
// std::string str = c.get<std::string>();
|
||||
// if (str.size() != 1)
|
||||
// continue;
|
||||
// end_chars_def.push_back(str[0]);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// session.items.reserve(items_json.size() + 1);
|
||||
// session.visible.reserve(items_json.size() + 1);
|
||||
// for (auto &item_json : items_json) {
|
||||
// CompletionItem item;
|
||||
// item.original = item_json;
|
||||
// item.label = item_json.value("label", "");
|
||||
// item.kind = item_json.value("kind", 0);
|
||||
// if (item_json.contains("detail") && item_json["detail"].is_string())
|
||||
// item.detail = item_json["detail"].get<std::string>();
|
||||
// if (item_json.contains("documentation")) {
|
||||
// if (item_json["documentation"].is_string()) {
|
||||
// item.documentation = item_json["documentation"].get<std::string>();
|
||||
// } else if (item_json["documentation"].contains("value") &&
|
||||
// item_json["documentation"]["value"].is_string()) {
|
||||
// item.is_markup =
|
||||
// item_json["documentation"]["kind"].get<std::string>() ==
|
||||
// "markdown";
|
||||
// std::string documentation =
|
||||
// item_json["documentation"]["value"].get<std::string>();
|
||||
// if (documentation.size() > 1024)
|
||||
// item.is_markup = false;
|
||||
// if (item.is_markup) {
|
||||
// item.documentation =
|
||||
// substitute_fence(documentation, editor->lang.name);
|
||||
// } else {
|
||||
// item.documentation = documentation;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (item_json.contains("deprecated") &&
|
||||
// item_json["deprecated"].is_boolean())
|
||||
// item.deprecated = item_json["deprecated"].get<bool>();
|
||||
// auto tags = item_json.value("tags", std::vector<int>());
|
||||
// for (auto tag : tags)
|
||||
// if (tag == 1)
|
||||
// item.deprecated = true;
|
||||
// item.sort = item_json.value("sortText", item.label);
|
||||
// item.filter = item_json.value("filterText", item.label);
|
||||
// if (item_json.contains("preselect") &&
|
||||
// item_json["preselect"].is_boolean() &&
|
||||
// item_json["preselect"].get<bool>())
|
||||
// session.select = session.items.size() - 1;
|
||||
// TextEdit edit;
|
||||
// if (item_json.contains("textEdit")) {
|
||||
// auto &te = item_json["textEdit"];
|
||||
// if (te.contains("newText")) {
|
||||
// edit.text = te.value("newText", "");
|
||||
// if (te.contains("replace")) {
|
||||
// edit.start.row = te["replace"]["start"]["line"];
|
||||
// edit.start.col = te["replace"]["start"]["character"];
|
||||
// edit.end.row = te["replace"]["end"]["line"];
|
||||
// edit.end.col = te["replace"]["end"]["character"];
|
||||
// } else if (te.contains("insert")) {
|
||||
// edit.start.row = te["insert"]["start"]["line"];
|
||||
// edit.start.col = te["insert"]["start"]["character"];
|
||||
// edit.end.row = te["insert"]["end"]["line"];
|
||||
// edit.end.col = te["insert"]["end"]["character"];
|
||||
// } else if (te.contains("range")) {
|
||||
// edit.start.row = te["range"]["start"]["line"];
|
||||
// edit.start.col = te["range"]["start"]["character"];
|
||||
// edit.end.row = te["range"]["end"]["line"];
|
||||
// edit.end.col = te["range"]["end"]["character"];
|
||||
// } else {
|
||||
// edit.start = session.hook;
|
||||
// edit.end = editor->cursor;
|
||||
// }
|
||||
// }
|
||||
// } else if (item_json.contains("insertText") &&
|
||||
// item_json["insertText"].is_string()) {
|
||||
// edit.text = item_json["insertText"].get<std::string>();
|
||||
// edit.start = session.hook;
|
||||
// edit.end = editor->cursor;
|
||||
// } else {
|
||||
// edit.text = item.label;
|
||||
// edit.start = session.hook;
|
||||
// edit.end = editor->cursor;
|
||||
// }
|
||||
// editor->utf8_normalize_edit(&edit);
|
||||
// item.edits.push_back(edit);
|
||||
// if (item_json.contains("additionalTextEdits")) {
|
||||
// for (auto &te : item_json["additionalTextEdits"]) {
|
||||
// TextEdit edit;
|
||||
// edit.text = te.value("newText", "");
|
||||
// edit.start.row = te["range"]["start"]["line"];
|
||||
// edit.start.col = te["range"]["start"]["character"];
|
||||
// edit.end.row = te["range"]["end"]["line"];
|
||||
// edit.end.col = te["range"]["end"]["character"];
|
||||
// editor->utf8_normalize_edit(&edit);
|
||||
// item.edits.push_back(edit);
|
||||
// }
|
||||
// }
|
||||
// item.snippet = insert_text_format == 2;
|
||||
// if (item_json.contains("insertTextFormat"))
|
||||
// item.snippet = item_json["insertTextFormat"].get<int>() == 2;
|
||||
// if (item_json.contains("insertTextMode"))
|
||||
// item.asis = item_json["insertTextMode"].get<int>() == 1;
|
||||
// if (item_json.contains("commitCharacters"))
|
||||
// for (auto &c : item_json["commitCharacters"])
|
||||
// if (c.is_string() && c.get<std::string>().size() == 1)
|
||||
// item.end_chars.push_back(c.get<std::string>()[0]);
|
||||
// session.items.push_back(std::move(item));
|
||||
// session.visible.push_back(session.items.size() - 1);
|
||||
// }
|
||||
// session.box.hidden = false;
|
||||
// session.box.render_update();
|
||||
// };
|
||||
// LineIterator *it = begin_l_iter(editor->root, hook.row);
|
||||
// uint32_t length;
|
||||
// char *line = next_line(it, &length);
|
||||
// if (!line) {
|
||||
// free(it->buffer);
|
||||
// free(it);
|
||||
// return;
|
||||
// }
|
||||
// uint32_t col = utf8_offset_to_utf16(line, length, editor->cursor.col);
|
||||
// free(it->buffer);
|
||||
// free(it);
|
||||
// json message = {
|
||||
// {"jsonrpc", "2.0"},
|
||||
// {"method", "textDocument/completion"},
|
||||
// {"params",
|
||||
// {{"textDocument", {{"uri", editor->uri}}},
|
||||
// {"position", {{"line", editor->cursor.row}, {"character", col}}}}}};
|
||||
// if (editor->completion.trigger > 0) {
|
||||
// json context = {{"triggerKind", editor->completion.trigger}};
|
||||
// if (editor->completion.trigger == 2 && editor->completion.trigger_char)
|
||||
// context["triggerCharacter"] =
|
||||
// std::string(1, *editor->completion.trigger_char);
|
||||
// message["params"]["context"] = context;
|
||||
// }
|
||||
// lsp_send(editor->lsp, message, pending);
|
||||
// }
|
||||
//
|
||||
// // Move this into the box and merge the box and this guy
|
||||
// void CompletionSession::handle(KeyEvent event) {
|
||||
// if (!editor->lsp || !editor->lsp->allow_completion)
|
||||
// return;
|
||||
// if (mode != INSERT) {
|
||||
// this->active = false;
|
||||
// return;
|
||||
// }
|
||||
// std::unique_lock lock(this->mtx);
|
||||
// if (event.key_type == KEY_PASTE) {
|
||||
// this->active = false;
|
||||
// return;
|
||||
// } else if (event.key_type == KEY_CHAR) {
|
||||
// char ch = *event.c;
|
||||
// if (!this->active) {
|
||||
// for (char c : editor->lsp->trigger_chars)
|
||||
// if (c == ch) {
|
||||
// this->trigger = 2;
|
||||
// this->trigger_char = c;
|
||||
// completion_request(editor);
|
||||
// return;
|
||||
// }
|
||||
// } else {
|
||||
// if (!editor->completion.items.empty()) {
|
||||
// const auto &item =
|
||||
// editor->completion.items[editor->completion.select]; const
|
||||
// std::vector<char> &end_chars =
|
||||
// item.end_chars.empty() ? editor->lsp->end_chars : item.end_chars;
|
||||
// for (char c : end_chars)
|
||||
// if (c == ch) {
|
||||
// this->accept();
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
||||
// (ch >= '0' && ch <= '9') || ch == '_') {
|
||||
// if (this->active) {
|
||||
// if (this->complete)
|
||||
// completion_filter(editor);
|
||||
// else
|
||||
// completion_request(editor);
|
||||
// } else {
|
||||
// this->trigger = 3;
|
||||
// completion_request(editor);
|
||||
// }
|
||||
// } else if (ch == CTRL('\\')) {
|
||||
// if (this->active && !this->visible.empty()) {
|
||||
// this->accept();
|
||||
// } else {
|
||||
// this->trigger = 1;
|
||||
// completion_request(editor);
|
||||
// }
|
||||
// } else if (ch == CTRL('p')) {
|
||||
// if (this->active)
|
||||
// this->next();
|
||||
// } else if (ch == CTRL('o')) {
|
||||
// if (this->active)
|
||||
// this->prev();
|
||||
// } else if (ch == 0x7F || ch == 0x08 || ch == CTRL('W')) {
|
||||
// if (this->active) {
|
||||
// if (this->complete) {
|
||||
// if (editor->cursor <= this->hook)
|
||||
// this->active = false;
|
||||
// else
|
||||
// completion_filter(editor);
|
||||
// } else {
|
||||
// if (editor->cursor <= this->hook)
|
||||
// this->active = false;
|
||||
// else
|
||||
// completion_request(editor);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// this->active = false;
|
||||
// }
|
||||
// } else if (event.key_type == KEY_MOUSE && event.mouse_modifier == 0) {
|
||||
// // Prolly add mouse support here
|
||||
// // auto &box = this->box;
|
||||
// // if (event.mouse_y >= box.position.row &&
|
||||
// // event.mouse_x >= box.position.col) {
|
||||
// // uint32_t row = event.mouse_y - box.position.row;
|
||||
// // uint32_t col = event.mouse_x - box.position.col;
|
||||
// // if (row < box.size.row && col < box.size.col) {
|
||||
// // uint8_t idx = 0;
|
||||
// // /* TODO: fix index relative to scroll */
|
||||
// // complete_select(editor, idx);
|
||||
// // }
|
||||
// // }
|
||||
// // if it is being implemented then stop main event handler from
|
||||
// processing
|
||||
// // when click inside the box
|
||||
// this->active = false;
|
||||
// } else {
|
||||
// this->active = false;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void CompletionSession::resolve_doc() {
|
||||
// auto &item = this->items[this->select];
|
||||
// if (item.documentation)
|
||||
// return;
|
||||
// item.documentation = "";
|
||||
// LSPPending *pending = new LSPPending();
|
||||
// pending->editor = editor;
|
||||
// pending->callback = [](Editor *editor, const json &message) {
|
||||
// std::unique_lock lock(editor->completion.mtx);
|
||||
// auto &item = editor->completion.items[editor->completion.select];
|
||||
// if (message["result"].contains("documentation")) {
|
||||
// if (message["result"]["documentation"].is_string()) {
|
||||
// item.documentation =
|
||||
// message["result"]["documentation"].get<std::string>();
|
||||
// } else if (message["result"]["documentation"].contains("value") &&
|
||||
// message["result"]["documentation"]["value"].is_string()) {
|
||||
// item.is_markup =
|
||||
// message["result"]["documentation"]["kind"].get<std::string>() ==
|
||||
// "markdown";
|
||||
// std::string documentation =
|
||||
// message["result"]["documentation"]["value"].get<std::string>();
|
||||
// if (documentation.size() > 1024)
|
||||
// item.is_markup = false;
|
||||
// if (item.is_markup) {
|
||||
// item.documentation =
|
||||
// substitute_fence(documentation, editor->lang.name);
|
||||
// } else {
|
||||
// item.documentation = documentation;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// editor->completion.box.render_update();
|
||||
// };
|
||||
// json message = {{"jsonrpc", "2.0"},
|
||||
// {"method", "completionItem/resolve"},
|
||||
// {"params", item.original}};
|
||||
// lsp_send(editor->lsp, message, pending);
|
||||
// }
|
||||
//
|
||||
// void CompletionSession::accept() {
|
||||
// if (!this->active || this->box.hidden)
|
||||
// return;
|
||||
// auto &item = this->items[this->select];
|
||||
// // TODO: support snippets and asis here
|
||||
// // once indentation engine is implemented
|
||||
// if (this->version != editor->lsp_version) {
|
||||
// int delta_col = 0;
|
||||
// TextEdit &e = item.edits[0];
|
||||
// if (e.end.row == editor->cursor.row) {
|
||||
// delta_col = editor->cursor.col - e.end.col;
|
||||
// e.end.col = editor->cursor.col;
|
||||
// for (size_t i = 1; i < item.edits.size(); ++i) {
|
||||
// TextEdit &e = item.edits[i];
|
||||
// if (e.start.row == editor->cursor.row) {
|
||||
// e.start.col += delta_col;
|
||||
// if (e.end.row == editor->cursor.row)
|
||||
// e.end.col += delta_col;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// editor->apply_lsp_edits(item.edits, true);
|
||||
// this->active = false;
|
||||
// }
|
||||
//
|
||||
// inline static int visible_index(const CompletionSession &s) {
|
||||
// for (size_t i = 0; i < s.visible.size(); ++i)
|
||||
// if (s.visible[i] == s.select)
|
||||
// return (int)i;
|
||||
// return -1;
|
||||
// }
|
||||
//
|
||||
// void CompletionSession::next() {
|
||||
// if (!this->active || this->box.hidden || this->visible.empty())
|
||||
// return;
|
||||
// int vi = visible_index(*this);
|
||||
// if (vi < 0)
|
||||
// vi = 0;
|
||||
// else
|
||||
// vi = (vi + 1) % this->visible.size();
|
||||
// this->select = this->visible[vi];
|
||||
// this->resolve_doc();
|
||||
// completion_adjust_scroll(*this);
|
||||
// this->box.render_update();
|
||||
// }
|
||||
//
|
||||
// void CompletionSession::prev() {
|
||||
// if (!this->active || this->box.hidden || this->visible.empty())
|
||||
// return;
|
||||
// int vi = visible_index(*this);
|
||||
// if (vi < 0)
|
||||
// vi = 0;
|
||||
// else
|
||||
// vi = (vi + this->visible.size() - 1) % this->visible.size();
|
||||
// this->select = this->visible[vi];
|
||||
// this->resolve_doc();
|
||||
// completion_adjust_scroll(*this);
|
||||
// this->box.render_update();
|
||||
// }
|
||||
//
|
||||
// void CompletionSession::choose(uint8_t index) {
|
||||
// this->select = index;
|
||||
// this->accept();
|
||||
// }
|
||||
/* #include "editor/editor.h"
|
||||
#include "io/knot.h"
|
||||
#include "io/sysio.h"
|
||||
#include "main.h"
|
||||
#include "ui/completionbox.h"
|
||||
#include "utils/utils.h"
|
||||
|
||||
inline static std::string completion_prefix(Editor *editor) {
|
||||
Coord hook = editor->completion.hook;
|
||||
Coord cur = editor->cursor;
|
||||
if (hook.row != cur.row || cur.col < hook.col)
|
||||
return "";
|
||||
LineIterator *it = begin_l_iter(editor->root, hook.row);
|
||||
uint32_t line_len;
|
||||
char *line = next_line(it, &line_len);
|
||||
if (!line) {
|
||||
free(it->buffer);
|
||||
free(it);
|
||||
return "";
|
||||
}
|
||||
std::string prefix(line + hook.col, cur.col - hook.col);
|
||||
free(it->buffer);
|
||||
free(it);
|
||||
return prefix;
|
||||
}
|
||||
|
||||
inline static void completion_adjust_scroll(CompletionSession &s) {
|
||||
if (s.visible.empty())
|
||||
return;
|
||||
int vi = -1;
|
||||
for (size_t i = 0; i < s.visible.size(); i++)
|
||||
if (s.visible[i] == s.select) {
|
||||
vi = (int)i;
|
||||
break;
|
||||
}
|
||||
if (vi < 0)
|
||||
return;
|
||||
if ((uint32_t)vi < s.scroll)
|
||||
s.scroll = vi;
|
||||
else if ((uint32_t)vi >= s.scroll + 8)
|
||||
s.scroll = vi - 7;
|
||||
}
|
||||
|
||||
void completion_filter(Editor *editor) {
|
||||
auto &session = editor->completion;
|
||||
std::string prefix = completion_prefix(editor);
|
||||
session.visible.clear();
|
||||
for (size_t i = 0; i < session.items.size(); ++i) {
|
||||
const auto &item = session.items[i];
|
||||
const std::string &key = item.filter;
|
||||
if (key.size() >= prefix.size() &&
|
||||
key.compare(0, prefix.size(), prefix) == 0)
|
||||
session.visible.push_back(i);
|
||||
}
|
||||
if (session.visible.empty()) {
|
||||
session.box.hidden = true;
|
||||
return;
|
||||
}
|
||||
if (std::find(session.visible.begin(), session.visible.end(),
|
||||
session.select) == session.visible.end())
|
||||
session.select = session.visible[0];
|
||||
session.box.hidden = false;
|
||||
session.scroll = 0;
|
||||
completion_adjust_scroll(session);
|
||||
session.box.render_update();
|
||||
}
|
||||
|
||||
void completion_request(Editor *editor) {
|
||||
Coord hook = editor->cursor;
|
||||
editor->word_boundaries(editor->cursor, &hook.col, nullptr, nullptr,
|
||||
nullptr); editor->completion.hook = hook; editor->completion.complete =
|
||||
false; editor->completion.active = false; editor->completion.items.clear();
|
||||
editor->completion.visible.clear();
|
||||
editor->completion.select = 0;
|
||||
editor->completion.version = editor->lsp_version;
|
||||
LSPPending *pending = new LSPPending();
|
||||
pending->editor = editor;
|
||||
pending->callback = [](Editor *editor, const json &message) {
|
||||
auto &session = editor->completion;
|
||||
std::unique_lock lock(session.mtx);
|
||||
std::vector<json> items_json;
|
||||
std::vector<char> end_chars_def;
|
||||
int insert_text_format = 1;
|
||||
int insert_text_mode = 1;
|
||||
if (message.contains("result")) {
|
||||
auto &result = message["result"];
|
||||
if (result.is_array()) {
|
||||
items_json = result.get<std::vector<json>>();
|
||||
session.complete = true;
|
||||
if (items_json.empty())
|
||||
return;
|
||||
editor->completion.active = true;
|
||||
} else if (result.is_object() && result.contains("items")) {
|
||||
auto &list = result;
|
||||
items_json = list["items"].get<std::vector<json>>();
|
||||
if (items_json.empty())
|
||||
return;
|
||||
editor->completion.active = true;
|
||||
session.complete = !list.value("isIncomplete", false);
|
||||
if (list.contains("itemDefaults") &&
|
||||
list["itemDefaults"].is_object()) {
|
||||
auto &defs = list["itemDefaults"];
|
||||
if (defs.contains("insertTextFormat") &&
|
||||
defs["insertTextFormat"].is_number())
|
||||
insert_text_format = defs["insertTextFormat"].get<int>();
|
||||
if (defs.contains("insertTextMode") &&
|
||||
defs["insertTextMode"].is_number())
|
||||
insert_text_mode = defs["insertTextMode"].get<int>();
|
||||
if (defs.contains("textEdit"))
|
||||
if (defs["textEdit"].is_array())
|
||||
for (auto &c : defs["textEdit"]) {
|
||||
if (!c.is_string())
|
||||
continue;
|
||||
std::string str = c.get<std::string>();
|
||||
if (str.size() != 1)
|
||||
continue;
|
||||
end_chars_def.push_back(str[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
session.items.reserve(items_json.size() + 1);
|
||||
session.visible.reserve(items_json.size() + 1);
|
||||
for (auto &item_json : items_json) {
|
||||
CompletionItem item;
|
||||
item.original = item_json;
|
||||
item.label = item_json.value("label", "");
|
||||
item.kind = item_json.value("kind", 0);
|
||||
if (item_json.contains("detail") && item_json["detail"].is_string())
|
||||
item.detail = item_json["detail"].get<std::string>();
|
||||
if (item_json.contains("documentation")) {
|
||||
if (item_json["documentation"].is_string()) {
|
||||
item.documentation = item_json["documentation"].get<std::string>();
|
||||
} else if (item_json["documentation"].contains("value") &&
|
||||
item_json["documentation"]["value"].is_string()) {
|
||||
item.is_markup =
|
||||
item_json["documentation"]["kind"].get<std::string>() ==
|
||||
"markdown";
|
||||
std::string documentation =
|
||||
item_json["documentation"]["value"].get<std::string>();
|
||||
if (documentation.size() > 1024)
|
||||
item.is_markup = false;
|
||||
if (item.is_markup) {
|
||||
item.documentation =
|
||||
substitute_fence(documentation, editor->lang.name);
|
||||
} else {
|
||||
item.documentation = documentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item_json.contains("deprecated") &&
|
||||
item_json["deprecated"].is_boolean())
|
||||
item.deprecated = item_json["deprecated"].get<bool>();
|
||||
auto tags = item_json.value("tags", std::vector<int>());
|
||||
for (auto tag : tags)
|
||||
if (tag == 1)
|
||||
item.deprecated = true;
|
||||
item.sort = item_json.value("sortText", item.label);
|
||||
item.filter = item_json.value("filterText", item.label);
|
||||
if (item_json.contains("preselect") &&
|
||||
item_json["preselect"].is_boolean() &&
|
||||
item_json["preselect"].get<bool>())
|
||||
session.select = session.items.size() - 1;
|
||||
TextEdit edit;
|
||||
if (item_json.contains("textEdit")) {
|
||||
auto &te = item_json["textEdit"];
|
||||
if (te.contains("newText")) {
|
||||
edit.text = te.value("newText", "");
|
||||
if (te.contains("replace")) {
|
||||
edit.start.row = te["replace"]["start"]["line"];
|
||||
edit.start.col = te["replace"]["start"]["character"];
|
||||
edit.end.row = te["replace"]["end"]["line"];
|
||||
edit.end.col = te["replace"]["end"]["character"];
|
||||
} else if (te.contains("insert")) {
|
||||
edit.start.row = te["insert"]["start"]["line"];
|
||||
edit.start.col = te["insert"]["start"]["character"];
|
||||
edit.end.row = te["insert"]["end"]["line"];
|
||||
edit.end.col = te["insert"]["end"]["character"];
|
||||
} else if (te.contains("range")) {
|
||||
edit.start.row = te["range"]["start"]["line"];
|
||||
edit.start.col = te["range"]["start"]["character"];
|
||||
edit.end.row = te["range"]["end"]["line"];
|
||||
edit.end.col = te["range"]["end"]["character"];
|
||||
} else {
|
||||
edit.start = session.hook;
|
||||
edit.end = editor->cursor;
|
||||
}
|
||||
}
|
||||
} else if (item_json.contains("insertText") &&
|
||||
item_json["insertText"].is_string()) {
|
||||
edit.text = item_json["insertText"].get<std::string>();
|
||||
edit.start = session.hook;
|
||||
edit.end = editor->cursor;
|
||||
} else {
|
||||
edit.text = item.label;
|
||||
edit.start = session.hook;
|
||||
edit.end = editor->cursor;
|
||||
}
|
||||
editor->utf8_normalize_edit(&edit);
|
||||
item.edits.push_back(edit);
|
||||
if (item_json.contains("additionalTextEdits")) {
|
||||
for (auto &te : item_json["additionalTextEdits"]) {
|
||||
TextEdit edit;
|
||||
edit.text = te.value("newText", "");
|
||||
edit.start.row = te["range"]["start"]["line"];
|
||||
edit.start.col = te["range"]["start"]["character"];
|
||||
edit.end.row = te["range"]["end"]["line"];
|
||||
edit.end.col = te["range"]["end"]["character"];
|
||||
editor->utf8_normalize_edit(&edit);
|
||||
item.edits.push_back(edit);
|
||||
}
|
||||
}
|
||||
item.snippet = insert_text_format == 2;
|
||||
if (item_json.contains("insertTextFormat"))
|
||||
item.snippet = item_json["insertTextFormat"].get<int>() == 2;
|
||||
if (item_json.contains("insertTextMode"))
|
||||
item.asis = item_json["insertTextMode"].get<int>() == 1;
|
||||
if (item_json.contains("commitCharacters"))
|
||||
for (auto &c : item_json["commitCharacters"])
|
||||
if (c.is_string() && c.get<std::string>().size() == 1)
|
||||
item.end_chars.push_back(c.get<std::string>()[0]);
|
||||
session.items.push_back(std::move(item));
|
||||
session.visible.push_back(session.items.size() - 1);
|
||||
}
|
||||
session.box.hidden = false;
|
||||
session.box.render_update();
|
||||
};
|
||||
LineIterator *it = begin_l_iter(editor->root, hook.row);
|
||||
uint32_t length;
|
||||
char *line = next_line(it, &length);
|
||||
if (!line) {
|
||||
free(it->buffer);
|
||||
free(it);
|
||||
return;
|
||||
}
|
||||
uint32_t col = utf8_offset_to_utf16(line, length, editor->cursor.col);
|
||||
free(it->buffer);
|
||||
free(it);
|
||||
json message = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"method", "textDocument/completion"},
|
||||
{"params",
|
||||
{{"textDocument", {{"uri", editor->uri}}},
|
||||
{"position", {{"line", editor->cursor.row}, {"character", col}}}}}};
|
||||
if (editor->completion.trigger > 0) {
|
||||
json context = {{"triggerKind", editor->completion.trigger}};
|
||||
if (editor->completion.trigger == 2 && editor->completion.trigger_char)
|
||||
context["triggerCharacter"] =
|
||||
std::string(1, *editor->completion.trigger_char);
|
||||
message["params"]["context"] = context;
|
||||
}
|
||||
lsp_send(editor->lsp, message, pending);
|
||||
}
|
||||
|
||||
Move this into the box and merge the box and this guy
|
||||
void CompletionSession::handle(KeyEvent event) {
|
||||
if (!editor->lsp || !editor->lsp->allow_completion)
|
||||
return;
|
||||
if (mode != INSERT) {
|
||||
this->active = false;
|
||||
return;
|
||||
}
|
||||
std::unique_lock lock(this->mtx);
|
||||
if (event.key_type == KEY_PASTE) {
|
||||
this->active = false;
|
||||
return;
|
||||
} else if (event.key_type == KEY_CHAR) {
|
||||
char ch = *event.c;
|
||||
if (!this->active) {
|
||||
for (char c : editor->lsp->trigger_chars)
|
||||
if (c == ch) {
|
||||
this->trigger = 2;
|
||||
this->trigger_char = c;
|
||||
completion_request(editor);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!editor->completion.items.empty()) {
|
||||
const auto &item =
|
||||
editor->completion.items[editor->completion.select]; const
|
||||
std::vector<char> &end_chars =
|
||||
item.end_chars.empty() ? editor->lsp->end_chars : item.end_chars;
|
||||
for (char c : end_chars)
|
||||
if (c == ch) {
|
||||
this->accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
||||
(ch >= '0' && ch <= '9') || ch == '_') {
|
||||
if (this->active) {
|
||||
if (this->complete)
|
||||
completion_filter(editor);
|
||||
else
|
||||
completion_request(editor);
|
||||
} else {
|
||||
this->trigger = 3;
|
||||
completion_request(editor);
|
||||
}
|
||||
} else if (ch == CTRL('\\')) {
|
||||
if (this->active && !this->visible.empty()) {
|
||||
this->accept();
|
||||
} else {
|
||||
this->trigger = 1;
|
||||
completion_request(editor);
|
||||
}
|
||||
} else if (ch == CTRL('p')) {
|
||||
if (this->active)
|
||||
this->next();
|
||||
} else if (ch == CTRL('o')) {
|
||||
if (this->active)
|
||||
this->prev();
|
||||
} else if (ch == 0x7F || ch == 0x08 || ch == CTRL('W')) {
|
||||
if (this->active) {
|
||||
if (this->complete) {
|
||||
if (editor->cursor <= this->hook)
|
||||
this->active = false;
|
||||
else
|
||||
completion_filter(editor);
|
||||
} else {
|
||||
if (editor->cursor <= this->hook)
|
||||
this->active = false;
|
||||
else
|
||||
completion_request(editor);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->active = false;
|
||||
}
|
||||
} else if (event.key_type == KEY_MOUSE && event.mouse_modifier == 0) {
|
||||
Prolly add mouse support here
|
||||
auto &box = this->box;
|
||||
if (event.mouse_y >= box.position.row &&
|
||||
event.mouse_x >= box.position.col) {
|
||||
uint32_t row = event.mouse_y - box.position.row;
|
||||
uint32_t col = event.mouse_x - box.position.col;
|
||||
if (row < box.size.row && col < box.size.col) {
|
||||
uint8_t idx = 0;
|
||||
complete_select(editor, idx);
|
||||
}
|
||||
}
|
||||
if it is being implemented then stop main event handler from
|
||||
processing
|
||||
when click inside the box
|
||||
this->active = false;
|
||||
} else {
|
||||
this->active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void CompletionSession::resolve_doc() {
|
||||
auto &item = this->items[this->select];
|
||||
if (item.documentation)
|
||||
return;
|
||||
item.documentation = "";
|
||||
LSPPending *pending = new LSPPending();
|
||||
pending->editor = editor;
|
||||
pending->callback = [](Editor *editor, const json &message) {
|
||||
std::unique_lock lock(editor->completion.mtx);
|
||||
auto &item = editor->completion.items[editor->completion.select];
|
||||
if (message["result"].contains("documentation")) {
|
||||
if (message["result"]["documentation"].is_string()) {
|
||||
item.documentation =
|
||||
message["result"]["documentation"].get<std::string>();
|
||||
} else if (message["result"]["documentation"].contains("value") &&
|
||||
message["result"]["documentation"]["value"].is_string()) {
|
||||
item.is_markup =
|
||||
message["result"]["documentation"]["kind"].get<std::string>() ==
|
||||
"markdown";
|
||||
std::string documentation =
|
||||
message["result"]["documentation"]["value"].get<std::string>();
|
||||
if (documentation.size() > 1024)
|
||||
item.is_markup = false;
|
||||
if (item.is_markup) {
|
||||
item.documentation =
|
||||
substitute_fence(documentation, editor->lang.name);
|
||||
} else {
|
||||
item.documentation = documentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
editor->completion.box.render_update();
|
||||
};
|
||||
json message = {{"jsonrpc", "2.0"},
|
||||
{"method", "completionItem/resolve"},
|
||||
{"params", item.original}};
|
||||
lsp_send(editor->lsp, message, pending);
|
||||
}
|
||||
|
||||
void CompletionSession::accept() {
|
||||
if (!this->active || this->box.hidden)
|
||||
return;
|
||||
auto &item = this->items[this->select];
|
||||
TODO: support snippets and asis here
|
||||
once indentation engine is implemented
|
||||
if (this->version != editor->lsp_version) {
|
||||
int delta_col = 0;
|
||||
TextEdit &e = item.edits[0];
|
||||
if (e.end.row == editor->cursor.row) {
|
||||
delta_col = editor->cursor.col - e.end.col;
|
||||
e.end.col = editor->cursor.col;
|
||||
for (size_t i = 1; i < item.edits.size(); ++i) {
|
||||
TextEdit &e = item.edits[i];
|
||||
if (e.start.row == editor->cursor.row) {
|
||||
e.start.col += delta_col;
|
||||
if (e.end.row == editor->cursor.row)
|
||||
e.end.col += delta_col;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
editor->apply_lsp_edits(item.edits, true);
|
||||
this->active = false;
|
||||
}
|
||||
|
||||
inline static int visible_index(const CompletionSession &s) {
|
||||
for (size_t i = 0; i < s.visible.size(); ++i)
|
||||
if (s.visible[i] == s.select)
|
||||
return (int)i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void CompletionSession::next() {
|
||||
if (!this->active || this->box.hidden || this->visible.empty())
|
||||
return;
|
||||
int vi = visible_index(*this);
|
||||
if (vi < 0)
|
||||
vi = 0;
|
||||
else
|
||||
vi = (vi + 1) % this->visible.size();
|
||||
this->select = this->visible[vi];
|
||||
this->resolve_doc();
|
||||
completion_adjust_scroll(*this);
|
||||
this->box.render_update();
|
||||
}
|
||||
|
||||
void CompletionSession::prev() {
|
||||
if (!this->active || this->box.hidden || this->visible.empty())
|
||||
return;
|
||||
int vi = visible_index(*this);
|
||||
if (vi < 0)
|
||||
vi = 0;
|
||||
else
|
||||
vi = (vi + this->visible.size() - 1) % this->visible.size();
|
||||
this->select = this->visible[vi];
|
||||
this->resolve_doc();
|
||||
completion_adjust_scroll(*this);
|
||||
this->box.render_update();
|
||||
}
|
||||
|
||||
void CompletionSession::choose(uint8_t index) {
|
||||
this->select = index;
|
||||
this->accept();
|
||||
}
|
||||
*/
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
-68
@@ -1,5 +1,4 @@
|
||||
#include "editor/editor.h"
|
||||
#include "editor/decl.h"
|
||||
#include "lsp/lsp.h"
|
||||
#include "main.h"
|
||||
#include "syntax/langs.h"
|
||||
@@ -58,7 +57,6 @@ Editor::~Editor() {
|
||||
void Editor::save() {
|
||||
if (!this->root)
|
||||
return;
|
||||
int version = this->lsp_version;
|
||||
uint32_t char_count = this->root->char_count;
|
||||
char *str = read(this->root, 0, char_count);
|
||||
if (!str)
|
||||
@@ -77,70 +75,4 @@ void Editor::save() {
|
||||
free(str);
|
||||
ui::bar.log("Written " + std::to_string(char_count) + " bytes to " +
|
||||
this->filename);
|
||||
auto lsp = this->lsp.load();
|
||||
if (lsp) {
|
||||
log("Saving %s", this->filename.c_str());
|
||||
auto message = std::make_unique<LSPMessage>();
|
||||
message->message = {{"method", "textDocument/didSave"},
|
||||
{"params", {{"textDocument", {{"uri", this->uri}}}}}};
|
||||
lsp->send(std::move(message));
|
||||
if (lsp->allow_formatting) {
|
||||
log("Formatting %s", this->filename.c_str());
|
||||
json s_msg = {{"method", "textDocument/formatting"},
|
||||
{"params",
|
||||
{{"textDocument", {{"uri", this->uri}}},
|
||||
{"options",
|
||||
{{"tabSize", 2},
|
||||
{"insertSpaces", true},
|
||||
{"trimTrailingWhitespace", true},
|
||||
{"trimFinalNewlines", true}}}}}};
|
||||
auto save_msg = std::make_unique<LSPMessage>();
|
||||
save_msg->editor = this;
|
||||
save_msg->message = s_msg;
|
||||
save_msg->callback = [s_msg, version](const LSPMessage &msg) {
|
||||
log("Formattin");
|
||||
if (version != msg.editor->lsp_version)
|
||||
return;
|
||||
auto &edits = msg.message["result"];
|
||||
if (edits.is_array()) {
|
||||
std::vector<TextEdit> t_edits;
|
||||
t_edits.reserve(edits.size());
|
||||
for (auto &edit : edits) {
|
||||
TextEdit t_edit;
|
||||
t_edit.text = edit.value("newText", "");
|
||||
t_edit.start.row = edit["range"]["start"]["line"];
|
||||
t_edit.start.col = edit["range"]["start"]["character"];
|
||||
t_edit.end.row = edit["range"]["end"]["line"];
|
||||
t_edit.end.col = edit["range"]["end"]["character"];
|
||||
msg.editor->utf8_normalize_edit(&t_edit);
|
||||
t_edits.push_back(t_edit);
|
||||
}
|
||||
msg.editor->apply_lsp_edits(t_edits, false);
|
||||
msg.editor->ensure_cursor();
|
||||
uint32_t char_count = msg.editor->root->char_count;
|
||||
char *str = read(msg.editor->root, 0, char_count);
|
||||
if (!str)
|
||||
return;
|
||||
std::ofstream out(msg.editor->filename);
|
||||
if (!msg.editor->unix_eol) {
|
||||
for (uint32_t i = 0; i < char_count; ++i) {
|
||||
if (str[i] == '\n')
|
||||
out.put('\r');
|
||||
out.put(str[i]);
|
||||
}
|
||||
} else {
|
||||
out.write(str, char_count);
|
||||
}
|
||||
out.close();
|
||||
free(str);
|
||||
auto save_msg = std::make_unique<LSPMessage>();
|
||||
save_msg->editor = msg.editor;
|
||||
save_msg->message = s_msg;
|
||||
save_msg->callback = [](const LSPMessage &) {};
|
||||
msg.editor->lsp.load()->send(std::move(save_msg));
|
||||
}
|
||||
};
|
||||
lsp->send(std::move(save_msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
+69
-8
@@ -238,6 +238,73 @@ void Editor::insert_char(char c) {
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::format_full(int version) {
|
||||
auto lsp = this->lsp.load();
|
||||
if (lsp) {
|
||||
log("Saving %s", this->filename.c_str());
|
||||
auto message = std::make_unique<LSPMessage>();
|
||||
message->message = {{"method", "textDocument/didSave"},
|
||||
{"params", {{"textDocument", {{"uri", this->uri}}}}}};
|
||||
lsp->send(std::move(message));
|
||||
if (lsp->allow_formatting) {
|
||||
json s_msg = {{"method", "textDocument/formatting"},
|
||||
{"params",
|
||||
{{"textDocument", {{"uri", this->uri}}},
|
||||
{"options",
|
||||
{{"tabSize", 2},
|
||||
{"insertSpaces", true},
|
||||
{"trimTrailingWhitespace", true},
|
||||
{"trimFinalNewlines", true}}}}}};
|
||||
auto save_msg = std::make_unique<LSPMessage>();
|
||||
save_msg->editor = this;
|
||||
save_msg->message = s_msg;
|
||||
save_msg->callback = [s_msg, version](const LSPMessage &msg) {
|
||||
if (version != msg.editor->lsp_version)
|
||||
return;
|
||||
auto &edits = msg.message["result"];
|
||||
if (edits.is_array()) {
|
||||
std::vector<TextEdit> t_edits;
|
||||
t_edits.reserve(edits.size());
|
||||
for (auto &edit : edits) {
|
||||
TextEdit t_edit;
|
||||
t_edit.text = edit.value("newText", "");
|
||||
t_edit.start.row = edit["range"]["start"]["line"];
|
||||
t_edit.start.col = edit["range"]["start"]["character"];
|
||||
t_edit.end.row = edit["range"]["end"]["line"];
|
||||
t_edit.end.col = edit["range"]["end"]["character"];
|
||||
msg.editor->utf8_normalize_edit(&t_edit);
|
||||
t_edits.push_back(t_edit);
|
||||
}
|
||||
msg.editor->apply_lsp_edits(t_edits, false);
|
||||
msg.editor->ensure_cursor();
|
||||
uint32_t char_count = msg.editor->root->char_count;
|
||||
char *str = read(msg.editor->root, 0, char_count);
|
||||
if (!str)
|
||||
return;
|
||||
std::ofstream out(msg.editor->filename);
|
||||
if (!msg.editor->unix_eol) {
|
||||
for (uint32_t i = 0; i < char_count; ++i) {
|
||||
if (str[i] == '\n')
|
||||
out.put('\r');
|
||||
out.put(str[i]);
|
||||
}
|
||||
} else {
|
||||
out.write(str, char_count);
|
||||
}
|
||||
out.close();
|
||||
free(str);
|
||||
auto save_msg = std::make_unique<LSPMessage>();
|
||||
save_msg->editor = msg.editor;
|
||||
save_msg->message = s_msg;
|
||||
save_msg->callback = [](const LSPMessage &) {};
|
||||
msg.editor->lsp.load()->send(std::move(save_msg));
|
||||
}
|
||||
};
|
||||
lsp->send(std::move(save_msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::normal_mode() {
|
||||
Coord prev_pos = this->cursor;
|
||||
mode = NORMAL;
|
||||
@@ -431,19 +498,13 @@ void Editor::handle_click(KeyEvent event, Coord size) {
|
||||
case SCROLL:
|
||||
switch (event.mouse_direction) {
|
||||
case SCROLL_UP:
|
||||
this->scroll_up(4);
|
||||
this->scroll_up(1);
|
||||
this->ensure_cursor();
|
||||
break;
|
||||
case SCROLL_DOWN:
|
||||
this->scroll_down(4);
|
||||
this->scroll_down(1);
|
||||
this->ensure_cursor();
|
||||
break;
|
||||
case SCROLL_LEFT:
|
||||
this->cursor_left(10);
|
||||
break;
|
||||
case SCROLL_RIGHT:
|
||||
this->cursor_right(10);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case PRESS:
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+1
-1
@@ -9,7 +9,7 @@ void Editor::render(std::vector<ScreenCell> &buffer, Coord size, Coord pos) {
|
||||
uint32_t sel_start = 0, sel_end = 0;
|
||||
uint32_t numlen =
|
||||
EXTRA_META + static_cast<int>(std::log10(this->root->line_count + 1));
|
||||
uint32_t render_width = size.col - numlen;
|
||||
uint32_t render_width = size.col - numlen - 1;
|
||||
uint32_t render_x = pos.col + numlen + 1;
|
||||
this->hooks.start_iter(this->scroll.row);
|
||||
auto warn_it = this->warnings.begin();
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Reference in New Issue
Block a user