joeydev/src/editor.c

601 lines
17 KiB
C

/*
* JoeyDev
* Copyright (C) 2018-2023 Scott Duensing <scott@kangaroopunch.com>
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
*/
#include "common.h"
#include "editor.h"
#include "utils.h"
#include "scintillaHeaders.h"
#define MARKER_ERROR_ARROW 0
#define MARKER_ERROR_HIGHLIGHT 1
typedef struct EditorDataS {
WindowDataT windowData;
GtkWidget *boxForEditor;
GtkWidget *statusBar;
GtkWidget *editor;
ScintillaObject *sci;
void *pLexer;
int id;
int statusBarId;
} EditorDataT;
static int _nextEditorId = 0;
static void clearEditor(EditorDataT *self);
EVENT void editorEditorNotify(GtkWidget *sciWidget, gint ctrlID, struct SCNotification *notifyData, gpointer userData);
static void loadEditor(EditorDataT *self);
static void loadEditorConfig(char *lexer, EditorDataT *self);
EVENT void menuEditorEditCopy(GtkWidget *object, gpointer userData);
EVENT void menuEditorEditCut(GtkWidget *object, gpointer userData);
EVENT void menuEditorEditDelete(GtkWidget *object, gpointer userData);
EVENT void menuEditorEditPaste(GtkWidget *object, gpointer userData);
EVENT void menuEditorFileClose(GtkWidget *object, gpointer userData);
EVENT void menuEditorFileNew(GtkWidget *object, gpointer userData);
EVENT void menuEditorFileOpen(GtkWidget *object, gpointer userData);
EVENT void menuEditorFileSave(GtkWidget *object, gpointer userData);
EVENT void menuEditorFileSaveAs(GtkWidget *object, gpointer userData);
EVENT void menuEditorHelpEditor(GtkWidget *object, gpointer userData);
static void status(EditorDataT *self, char *message);
EVENT gboolean winEditorClose(GtkWidget *object, gpointer userData);
static void winEditorDelete(gpointer userData);
static void writeEditorConfig(char *lexer, EditorDataT *self);
static void clearEditor(EditorDataT *self) {
(void)self;
// Clear editor.
SSM(SCI_CLEARALL, 0, 0);
// Clear error markers.
SSM(SCI_MARKERDELETEALL, MARKER_ERROR_ARROW, 0);
SSM(SCI_MARKERDELETEALL, MARKER_ERROR_HIGHLIGHT, 0);
}
EVENT void editorEditorNotify(GtkWidget *sciWidget, gint ctrlID, struct SCNotification *notifyData, gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
int lineNumber = (int)SSM(SCI_LINEFROMPOSITION, (uptr_t)notifyData->position, (sptr_t)0);
(void)sciWidget;
(void)ctrlID;
switch (notifyData->nmhdr.code) {
case SCN_MODIFIED:
if (notifyData->modificationType & SC_MOD_INSERTTEXT || notifyData->modificationType & SC_MOD_DELETETEXT) {
// Mark text dirty. SCN_SAVEPOINTLEFT isn't being reliable.
utilSetDirty((WindowDataT *)self, TRUE);
}
break;
case SCN_MARGINCLICK:
switch (notifyData->margin) {
case MARGIN_SCRIPT_FOLD_INDEX:
SSM(SCI_TOGGLEFOLD, lineNumber, (sptr_t)0);
break;
}
break;
}
}
static void loadEditor(EditorDataT *self) {
FILE *in = NULL;
char *line = NULL;
size_t len = 0;
in = fopen(self->windowData.filename, "rt");
if (in != NULL) {
utilEnsureBufferSize((unsigned char **)&line, (int *)&len, 1024); // Not technically needed, but fixes a pointer warning from memmaker.
while (getline(&line, &len, in) != -1) {
SSM(SCI_ADDTEXT, strlen(line), (sptr_t)line);
}
fclose(in);
DEL(line);
}
// Do again - loading text marks us dirty.
utilSetDirty((WindowDataT *)self, FALSE);
}
static void loadEditorConfig(char *lexer, EditorDataT *self) {
char *config = NULL;
FILE *in = NULL;
char *line = NULL;
size_t len = 0;
char *c = NULL;
char *type = NULL;
char *name = NULL;
unsigned int number;
unsigned int integer;
config = utilCreateString("%s%cjoeydev%ceditor-%s.conf", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR, lexer);
if (!utilFileExists(config)) {
// Nothing to load.
DEL(config);
return;
}
in = fopen(config, "rt");
DEL(config);
if (in) {
// Load config.
utilEnsureBufferSize((unsigned char **)&line, (int *)&len, 4096); // Not technically needed, but fixes a pointer warning from memmaker.
while (getline(&line, &len, in) != -1) {
if (strlen(line) > 0) line[strlen(line) - 1] = 0;
c = utilGetToken(line, " ", "\"", "\"");
utilDequote(c);
// Is this a 'style' line?
if (strcasecmp(c, "style") == 0) {
// style number "tags" "description" ... (style keywords)
c = utilGetToken(NULL, " ", "\"", "\"");
number = strtol(c, NULL, 10);
c = utilGetToken(NULL, " ", "\"", "\"");
c = utilGetToken(NULL, " ", "\"", "\"");
c = utilGetToken(NULL, " ", "\"", "\"");
do {
if (strcmp(c, "fore") == 0) {
c = utilGetToken(NULL, " ", "\"", "\"");
integer = strtol(c, NULL, 16);
SSM(SCI_STYLESETFORE, number, integer);
}
if (strcmp(c, "back") == 0) {
c = utilGetToken(NULL, " ", "\"", "\"");
integer = strtol(c, NULL, 16);
SSM(SCI_STYLESETBACK, number, integer);
}
if (strcmp(c, "bold") == 0) {
SSM(SCI_STYLESETBOLD, number, 1);
}
if (strcmp(c, "italic") == 0) {
SSM(SCI_STYLESETITALIC, number, 1);
}
c = utilGetToken(NULL, " ", "\"", "\"");
} while (c != NULL);
continue;
}
// Is this a 'property' line?
if (strcasecmp(c, "property") == 0) {
// property type name "description" value
c = utilGetToken(NULL, " ", "\"", "\"");
type = strdup(c);
c = utilGetToken(NULL, " ", "\"", "\"");
name = strdup(c);
c = utilGetToken(NULL, " ", "\"", "\"");
c = utilGetToken(NULL, " ", "\"", "\"");
utilDequote(c);
SSM(SCI_SETPROPERTY, (sptr_t)name, (sptr_t)c);
DEL(name);
DEL(type);
continue;
}
// Is this a 'keywords' line?
if (strcasecmp(c, "keywords") == 0) {
// keywords number "description" wordlist
c = utilGetToken(NULL, " ", "\"", "\"");
number = strtol(c, NULL, 10);
c = utilGetToken(NULL, " ", "\"", "\"");
c = utilGetToken(NULL, "\n", "\"", "\"");
if (c && strlen(c) > 0) SSM(SCI_SETKEYWORDS, number, (sptr_t)c);
continue;
}
}
DEL(line);
fclose(in);
}
}
EVENT void menuEditorEditCopy(GtkWidget *object, gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
(void)object;
SSM(SCI_COPY, 0, 0);
}
EVENT void menuEditorEditCut(GtkWidget *object, gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
(void)object;
SSM(SCI_CUT, 0, 0);
}
EVENT void menuEditorEditDelete(GtkWidget *object, gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
(void)object;
SSM(SCI_CLEAR, 0, 0);
}
EVENT void menuEditorEditPaste(GtkWidget *object, gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
(void)object;
SSM(SCI_PASTE, 0, 0);
}
EVENT void menuEditorFileClose(GtkWidget *object, gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
(void)object;
gtk_window_close(GTK_WINDOW(self->windowData.window));
}
EVENT void menuEditorFileNew(GtkWidget *object, gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
(void)object;
if (self->windowData.isDirty == TRUE) {
if (!utilQuestionDialog(self->windowData.window, "New", "You have unsaved changes. Start new?")) {
return;
}
status(self, "New image.");
}
clearEditor(self);
// Clear any filename.
DEL(self->windowData.filename);
// Mark clean.
utilSetDirty((WindowDataT *)self, FALSE);
}
EVENT void menuEditorFileOpen(GtkWidget *object, gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
(void)object;
if (utilFileOpen((WindowDataT *)userData, "*.*", "Code")) {
clearEditor(self);
loadEditor(self);
}
}
EVENT void menuEditorFileSave(GtkWidget *object, gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
int length = SSM(SCI_GETLENGTH, 0, 0);
FILE *out = NULL;
char *code = NULL;
// Do we need to save?
if (self->windowData.isDirty == TRUE) {
// Do we have a filename? If not, kick 'em to SaveAs.
if (self->windowData.filename == NULL) {
menuEditorFileSaveAs(object, userData);
return;
}
// Allocate space to fetch code from editor.
code = (char *)malloc(length + 1);
if (!code) {
//***TODO*** Something bad happened.
return;
}
// Fetch code.
SSM(SCI_GETTEXT, length, (sptr_t)code);
out = fopen(self->windowData.filename, "wt");
if (out != NULL) {
// Save!
fprintf(out, "%s\n", code);
fclose(out);
status(self, "Saved.");
// We're clean now.
utilSetDirty((WindowDataT *)self, FALSE);
} else {
//***TODO*** Something bad happened.
}
// Release code.
DEL(code);
}
}
EVENT void menuEditorFileSaveAs(GtkWidget *object, gpointer userData) {
(void)object;
if (utilFileSaveAs((WindowDataT *)userData, "*.*", "Code")) {
menuEditorFileSave(object, (EditorDataT *)userData);
}
}
EVENT void menuEditorHelpEditor(GtkWidget *object, gpointer userData) {
(void)object;
(void)userData;
gtk_show_uri_on_window(NULL, "https://skunkworks.kangaroopunch.com/skunkworks/joeydev/-/wikis/Code-Editor", GDK_CURRENT_TIME, NULL);
}
static void status(EditorDataT *self, char *message) {
gtk_statusbar_remove_all(GTK_STATUSBAR(self->statusBar), self->statusBarId);
gtk_statusbar_push(GTK_STATUSBAR(self->statusBar), self->statusBarId, message);
}
EVENT gboolean winEditorClose(GtkWidget *object, gpointer userData) {
// userData is not reliable due to menuVectorFileClose and util indirectly calling us.
EditorDataT *self = (EditorDataT *)utilGetWindowData(object);
(void)userData;
if (self->windowData.isDirty == TRUE) {
if (utilQuestionDialog(self->windowData.window, "Exit", "You have unsaved changes. Exit?")) {
winEditorDelete(self);
return FALSE;
}
return TRUE;
}
winEditorDelete(self);
return FALSE;
}
void winEditorCreate(char *filename) {
EditorDataT *self;
char *widgetNames[] = {
"winEditor",
"boxEditorForEditor",
"statusEditor",
NULL
};
GtkWidget **widgets[] = {
NULL,
NULL,
NULL
};
// Set up instance data.
self = NEW(EditorDataT);
self->windowData.closeWindow = winEditorClose;
// Load widgets from XML.
widgets[0] = &self->windowData.window;
widgets[1] = &self->boxForEditor;
widgets[2] = &self->statusBar;
utilGetWidgetsFromMemory("/com/kangaroopunch/joeydev/Editor.glade", widgetNames, widgets, self);
// Register window.
utilWindowRegister(self);
// Get status bar context ID.
self->statusBarId = gtk_statusbar_get_context_id(GTK_STATUSBAR(self->statusBar), "JoeyDev");
// Create Scintilla editor.
self->editor = scintilla_new();
self->sci = SCINTILLA(self->editor);
self->id = _nextEditorId++;
scintilla_set_id(self->sci, self->id);
gtk_widget_set_halign(self->editor, GTK_ALIGN_FILL);
gtk_widget_set_valign(self->editor, GTK_ALIGN_FILL);
gtk_widget_set_hexpand(self->editor, TRUE);
gtk_widget_set_vexpand(self->editor, TRUE);
gtk_box_set_child_packing(GTK_BOX(self->boxForEditor), self->editor, TRUE, TRUE, 0, GTK_PACK_START);
gtk_container_add(GTK_CONTAINER(self->boxForEditor), self->editor);
// Configure editor.
SSM(SCI_SETCODEPAGE, SC_CP_UTF8, 0);
SSM(SCI_SETIMEINTERACTION, SC_IME_WINDOWED, 0);
SSM(SCI_STYLESETCHARACTERSET, STYLE_DEFAULT, SC_CHARSET_DEFAULT);
SSM(SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)"Monospace");
SSM(SCI_STYLESETSIZEFRACTIONAL, STYLE_DEFAULT, 11 * SC_FONT_SIZE_MULTIPLIER);
SSM(SCI_STYLESETFORE, STYLE_DEFAULT, 0xFFFFFF);
SSM(SCI_STYLESETBACK, STYLE_DEFAULT, 0);
SSM(SCI_STYLECLEARALL, 0, 0);
SSM(SCI_SETTABWIDTH, 3, 0);
//SSM(SCI_SETEOLMODE, SC_EOL_CR, 0);
SSM(SCI_SETMARGINWIDTHN, 0, (int)SSM(SCI_TEXTWIDTH, STYLE_LINENUMBER, (sptr_t)"_99999"));
SSM(SCI_SETMARGINWIDTHN, 1, 16);
SSM(SCI_SETWRAPMODE, SC_WRAP_NONE, 0);
SSM(SCI_SETCARETSTYLE, CARETSTYLE_BLOCK | CARETSTYLE_OVERSTRIKE_BLOCK, 0);
SSM(SCI_SETCARETFORE, 0x00ffff, 0);
SSM(SCI_STYLESETBACK, STYLE_LINENUMBER, 0x222222);
SSM(SCI_SETFOLDMARGINCOLOUR, 1, 0x222222);
SSM(SCI_SETFOLDMARGINHICOLOUR, 1, 0x222222);
SSM(SCI_SETMARGINTYPEN, MARGIN_SCRIPT_FOLD_INDEX, SC_MARGIN_SYMBOL);
SSM(SCI_SETMARGINMASKN, MARGIN_SCRIPT_FOLD_INDEX, SC_MASK_FOLDERS);
SSM(SCI_SETMARGINWIDTHN, MARGIN_SCRIPT_FOLD_INDEX, 20);
SSM(SCI_SETMARGINSENSITIVEN, MARGIN_SCRIPT_FOLD_INDEX, 1);
SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPEN, SC_MARK_BOXMINUS);
SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDER, SC_MARK_BOXPLUS);
SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERSUB, SC_MARK_VLINE);
SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERTAIL, SC_MARK_LCORNER);
SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEREND, SC_MARK_BOXPLUSCONNECTED);
SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPENMID, SC_MARK_BOXMINUSCONNECTED);
SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERMIDTAIL, SC_MARK_TCORNER);
SSM(SCI_SETFOLDFLAGS, SC_FOLDFLAG_LINEAFTER_CONTRACTED , 0);
// Margin markers.
SSM(SCI_MARKERDEFINE, MARKER_ERROR_ARROW, SC_MARK_SHORTARROW); // Error
SSM(SCI_MARKERSETBACK, MARKER_ERROR_ARROW, 255 | (0 << 8) | (0 << 16)); // RGB
SSM(SCI_MARKERDEFINE, MARKER_ERROR_HIGHLIGHT, SC_MARK_BACKGROUND); // Error
SSM(SCI_MARKERSETBACK, MARKER_ERROR_HIGHLIGHT, 127 | (0 << 8) | (0 << 16)); // RGB
// Add lexer for language support.
self->pLexer = CreateLexer("cpp");
SSM(SCI_SETILEXER, 0, (sptr_t)self->pLexer);
writeEditorConfig("cpp", self);
loadEditorConfig("cpp", self);
// Connect editor to our code.
g_signal_connect(G_OBJECT(self->editor), "sci-notify", G_CALLBACK(editorEditorNotify), self);
// Show window.
gtk_widget_show_all(self->windowData.window);
if (filename != NULL) {
self->windowData.filename = strdup(filename);
loadEditor(self);
}
}
static void winEditorDelete(gpointer userData) {
EditorDataT *self = (EditorDataT *)userData;
// Scintilla keeps sending events after we delete things it expects to still exist. Prevent that.
g_signal_handlers_disconnect_by_func(G_OBJECT(self->editor), G_CALLBACK(editorEditorNotify), self);
utilWindowUnRegister(userData);
DEL(self);
}
static void writeEditorConfig(char *lexer, EditorDataT *self) {
char *name = NULL;
char *tags = NULL;
char *description = NULL;
char *config = NULL;
FILE *out = NULL;
int result = -1;
int size = -1;
int x;
config = utilCreateString("%s%cjoeydev%ceditor-%s.conf", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR, lexer);
if (utilFileExists(config)) {
// Don't clobber existing files.
DEL(config);
return;
}
out = fopen(config, "wt");
DEL(config);
if (out) {
// Header
fprintf(out, "%s\n", LEXER_VERSION);
fprintf(out, "------------------------------------------------------------------------------\n");
// Styles
result = SSM(SCI_GETNAMEDSTYLES, 0, 0);
if (result > 0) {
for (x = 0; x < result; x++) {
size = SSM(SCI_NAMEOFSTYLE, x, 0);
name = (char *)malloc(size + 1);
SSM(SCI_NAMEOFSTYLE, x, (sptr_t)name);
size = SSM(SCI_TAGSOFSTYLE, x, 0);
tags = (char *)malloc(size + 1);
SSM(SCI_TAGSOFSTYLE, x, (sptr_t)tags);
size = SSM(SCI_DESCRIPTIONOFSTYLE, x, 0);
description = (char *)malloc(size + 1);
SSM(SCI_DESCRIPTIONOFSTYLE, x, (sptr_t)description);
if (strlen(tags) > 0) {
fprintf(out, "style %d \"%s\" \"%s\" fore 0xffffff\n", x, tags, description);
}
DEL(description);
DEL(tags);
DEL(name);
}
}
fprintf(out, "\n");
// Properties
size = SSM(SCI_PROPERTYNAMES, 0, 0);
name = (char *)malloc(size + 1);
SSM(SCI_PROPERTYNAMES, 0, (sptr_t)name);
tags = strtok(name, "\n");
do {
size = SSM(SCI_DESCRIBEPROPERTY, (sptr_t)tags, 0);
description = (char *)malloc(size + 1);
SSM(SCI_DESCRIBEPROPERTY, (sptr_t)tags, (sptr_t)description);
result = SSM(SCI_PROPERTYTYPE, (sptr_t)tags, 0);
fprintf(out, "property ");
switch (result) {
case SC_TYPE_BOOLEAN:
fprintf(out, "boolean");
size = SSM(SCI_GETPROPERTYINT, (sptr_t)tags, 0);
config = utilCreateString("%d", size);
break;
case SC_TYPE_STRING:
fprintf(out, "string");
//***TODO*** This doesn't appear to be working.
size = SSM(SCI_GETPROPERTY, (sptr_t)tags, 0);
config = (char *)malloc(size + 1);
SSM(SCI_GETPROPERTY, (sptr_t)tags, (sptr_t)config);
break;
case SC_TYPE_INTEGER:
fprintf(out, "integer");
size = SSM(SCI_GETPROPERTYINT, (sptr_t)tags, 0);
config = utilCreateString("%d", size);
break;
}
fprintf(out, " %s \"%s\" %s\n", tags, description, config);
DEL(config);
DEL(description);
tags = strtok(NULL, "\n");
} while (tags != NULL);
DEL(name);
fprintf(out, "\n");
// Keyword Sets
size = SSM(SCI_DESCRIBEKEYWORDSETS, 0, 0);
name = (char *)malloc(size + 1);
SSM(SCI_DESCRIBEKEYWORDSETS, 0, (sptr_t)name);
tags = strtok(name, "\n");
size = 0;
do {
fprintf(out, "keywords %d \"%s\"\n", size, tags);
size++;
tags = strtok(NULL, "\n");
} while (tags != NULL);
DEL(name);
fclose(out);
}
}