FLUID: Refactors MergeBack

* moved functionality into its own files
* refactored all methods to be less than a page
* documented all calls
* tested all situations I could think of
This commit is contained in:
Matthias Melcher 2023-10-26 23:31:12 +02:00
parent 8663b86749
commit 0b408792c0
10 changed files with 670 additions and 318 deletions

View File

@ -38,6 +38,7 @@ set (CPPFILES
file.cxx
fluid_filename.cxx
function_panel.cxx
mergeback.cxx
pixmaps.cxx
shell_command.cxx
sourceview_panel.cxx
@ -71,6 +72,7 @@ set (HEADERFILES
factory.h
file.h
function_panel.h
mergeback.h
print_panel.h
pixmaps.h
shell_command.h

View File

@ -24,6 +24,7 @@
#include "code.h"
#include "function_panel.h"
#include "comments.h"
#include "mergeback.h"
#include <FL/fl_string_functions.h>
#include <FL/Fl_File_Chooser.H>
@ -353,6 +354,7 @@ static bool fd_isspace(int c) {
return (c>0 && c<128 && isspace(c));
}
// code duplication: see int is_id(char c) in code.cxx
static bool fd_iskeyword(int c) {
return (c>0 && c<128 && (isalnum(c) || c=='_'));
}

View File

@ -29,6 +29,7 @@
#include "code.h"
#include "Fluid_Image.h"
#include "custom_widgets.h"
#include "mergeback.h"
#include <FL/Fl.H>
#include <FL/fl_message.H>

View File

@ -27,6 +27,7 @@
#include "alignment_panel.h"
#include "widget_panel.h"
#include "undo.h"
#include "mergeback.h"
#include <FL/Fl.H>
#include <FL/Fl_Group.H>
@ -2631,7 +2632,7 @@ void live_mode_cb(Fl_Button*o,void *) {
}
// update the panel according to current widget set:
static void load_panel() {
void load_panel() {
if (!the_panel) return;
// find all the Fl_Widget subclasses currently selected:

View File

@ -39,6 +39,7 @@ CPPFILES = \
fluid.cxx \
fluid_filename.cxx \
function_panel.cxx \
mergeback.cxx \
pixmaps.cxx \
shell_command.cxx \
sourceview_panel.cxx \

View File

@ -33,7 +33,7 @@
#include <stdio.h>
#include <stdlib.h>
#include "zlib.h"
#include <zlib.h>
/// \defgroup cfile C Code File Operations
/// \{
@ -42,13 +42,19 @@
/**
Return true if c can be in a C identifier.
I needed this so it is not messed up by locale settings.
\param[in] c a character, or the start of a utf-8 sequence
\return 1 if c is alphanumeric or '_'
*/
int is_id(char c) {
return (c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9') || c=='_';
}
/**
Write a file that contains all label and tooltip strings for internationalisation.
Write a file that contains all label and tooltip strings for internationalization.
The user is responsible to set the right file name extension. The file format
is determined by `g_project.i18n_type`.
\param[in] filename file path and name to a file that will hold the strings
\return 1 if the file could not be opened for writing, or the result of `fclose`.
*/
int write_strings(const Fl_String &filename) {
Fl_Type *p;
@ -175,6 +181,7 @@ int write_strings(const Fl_String &filename) {
////////////////////////////////////////////////////////////////
// Generate unique but human-readable identifiers:
/** A binary searchable tree storing identifiers for quick retrieval. */
struct Fd_Identifier_Tree {
char* text;
void* object;
@ -286,6 +293,7 @@ const char *Fd_Code_Writer::indent_plus(int offset) {
// the header file. This is done by keeping a binary tree of all
// the calls so far and not printing it if it is in the tree.
/** A binary searchable tree storing text for quick retrieval. */
struct Fd_Text_Tree {
char *text;
Fd_Text_Tree *left, *right;
@ -302,6 +310,7 @@ Fd_Text_Tree::~Fd_Text_Tree() {
delete right;
}
/** A binary searchable tree storing pointers for quick retrieval. */
struct Fd_Pointer_Tree {
void *ptr;
Fd_Pointer_Tree *left, *right;
@ -319,7 +328,9 @@ Fd_Pointer_Tree::~Fd_Pointer_Tree() {
/**
Print a formatted line to the header file, unless the same line was produced before in this header file.
\note Resulting line is cropped at 1023 bytes.
\param[in] format printf-style formatting text, followed by a vararg list
\return 1 if the text was written to the file, 0 if it was previously written.
*/
int Fd_Code_Writer::write_h_once(const char *format, ...) {
va_list args;
@ -341,7 +352,9 @@ int Fd_Code_Writer::write_h_once(const char *format, ...) {
/**
Print a formatted line to the source file, unless the same line was produced before in this code file.
\note Resulting line is cropped at 1023 bytes.
\param[in] format printf-style formatting text, followed by a vararg list
\return 1 if the text was written to the file, 0 if it was previously written.
*/
int Fd_Code_Writer::write_c_once(const char *format, ...) {
va_list args;
@ -371,6 +384,8 @@ int Fd_Code_Writer::write_c_once(const char *format, ...) {
/**
Return true if this pointer was already included in the code file.
If it was not, add it to the list and return false.
\param[in] pp ay pointer
\return true if found in the tree, false if added to the tree
*/
bool Fd_Code_Writer::c_contains(void *pp) {
Fd_Pointer_Tree **p = &ptr_in_code;
@ -506,6 +521,9 @@ void Fd_Code_Writer::write_cstring(const char *s) {
Write an array of C binary data (does not add a null).
The output is bracketed in { and }. The content is written
as decimal bytes, i.e. `{ 1, 2, 200 }`
\param[in] s a block of binary data, interpreted as unsigned bytes
\param[in] length size of the block in bytes
*/
void Fd_Code_Writer::write_cdata(const char *s, int length) {
if (varused_test) {
@ -615,8 +633,11 @@ void Fd_Code_Writer::write_hc(const char *indent, int n, const char* c, const ch
/**
Write one or more lines of code, indenting each one of them.
\param[in] textlines one or more lines of text, separated by \\n
\param[in] inIndent increment indentation by this amount
\param[in] inTrailWith append this character if the last line did not end with
a newline, usually 0 or newline.
*/
void Fd_Code_Writer::write_c_indented(const char *textlines, int inIndent, char inTrailwWith) {
void Fd_Code_Writer::write_c_indented(const char *textlines, int inIndent, char inTrailWith) {
if (textlines) {
indentation += inIndent;
for (;;) {
@ -638,8 +659,8 @@ void Fd_Code_Writer::write_c_indented(const char *textlines, int inIndent, char
if (newline) {
write_c("\n");
} else {
if (inTrailwWith)
write_c("%c", inTrailwWith);
if (inTrailWith)
write_c("%c", inTrailWith);
break;
}
textlines = newline+1;
@ -651,9 +672,11 @@ void Fd_Code_Writer::write_c_indented(const char *textlines, int inIndent, char
/**
Recursively dump code, putting children between the two parts of the parent code.
\param[in] p write this type and all its children
\return pointer to the next sibling
*/
Fl_Type* Fd_Code_Writer::write_code(Fl_Type* p) {
// write all code that come before the children code
// write all code that comes before the children code
// (but don't write the last comment until the very end)
if (!(p==Fl_Type::last && p->is_a(ID_Comment))) {
if (write_sourceview) p->code1_start = (int)ftell(code_file);
@ -712,6 +735,8 @@ Fl_Type* Fd_Code_Writer::write_code(Fl_Type* p) {
If the files already exist, they will be overwritten.
\note There is no true error checking here.
\param[in] s filename of source code file
\param[in] t filename of the header file
\return 0 if the operation failed, 1 if it was successful
@ -881,6 +906,7 @@ int Fd_Code_Writer::write_code(const char *s, const char *t, bool to_sourceview)
/**
Write the public/private/protected keywords inside the class.
This avoids repeating these words if the mode is already set.
\param[in] state 0 for private, 1 for public, 2 for protected
*/
void Fd_Code_Writer::write_public(int state) {
if (!current_class && !current_widget_class) return;
@ -895,7 +921,9 @@ void Fd_Code_Writer::write_public(int state) {
}
}
/**
Create and initialize a new C++ source code writer.
*/
Fd_Code_Writer::Fd_Code_Writer()
: code_file(NULL),
header_file(NULL),
@ -904,9 +932,9 @@ Fd_Code_Writer::Fd_Code_Writer()
text_in_code(NULL),
ptr_in_code(NULL),
block_crc_(0),
block_line_start_(true),
block_buffer_(NULL),
block_buffer_size_(0),
block_line_start_(true),
indentation(0),
write_sourceview(false),
varused_test(0),
@ -915,6 +943,9 @@ Fd_Code_Writer::Fd_Code_Writer()
block_crc_ = crc32(0, NULL, 0);
}
/**
Release all resources.
*/
Fd_Code_Writer::~Fd_Code_Writer()
{
delete id_root;
@ -924,12 +955,31 @@ Fd_Code_Writer::~Fd_Code_Writer()
if (block_buffer_) ::free(block_buffer_);
}
/**
Write a MergeBack tag as a separate line of C++ comment.
The tag contains information about the type of tag that we are writing, a
link back to the type using its unique id, and the CRC of all code written
after the previous tag up to this point.
\param[in] type FD_TAG_GENERIC, FD_TAG_CODE, FD_TAG_MENU_CALLBACK, or FD_TAG_WIDGET_CALLBACK
\param[in] uid the unique id of the current type
*/
void Fd_Code_Writer::tag(int type, unsigned short uid) {
if (g_project.write_mergeback_data)
fprintf(code_file, "//~fl~%d~%04x~%08x~~\n", type, (int)uid, (unsigned int)block_crc_);
block_crc_ = crc32(0, NULL, 0);
}
/**
Static function to calculate the CRC32 of a block of C source code.
Calculation of the CRC ignores leading whitespace in a line and all linefeed
characters ('\\r').
\param[in] data a pointer to the data block
\param[in] n the size of the data in bytes, or -1 to use strlen()
\param[in] in_crc add to this CRC, 0 by default to start a new block
\param[inout] inout_line_start optional pointer to flag that determines
if we are the start of a line, used to find leading whitespace
\return the new CRC
*/
unsigned long Fd_Code_Writer::block_crc(const void *data, int n, unsigned long in_crc, bool *inout_line_start) {
if (!data) return 0;
if (n==-1) n = (int)strlen((const char*)data);
@ -953,10 +1003,19 @@ unsigned long Fd_Code_Writer::block_crc(const void *data, int n, unsigned long i
return in_crc;
}
/** Add the following block of text to the CRC of this class.
\param[in] data a pointer to the data block
\param[in] n the size of the data in bytes, or -1 to use strlen()
*/
void Fd_Code_Writer::crc_add(const void *data, int n) {
block_crc_ = block_crc(data, n, block_crc_, &block_line_start_);
}
/** Write formatted text to the code file.
If MergeBack is enabled, the CRC calculation is continued.
\param[in] format printf style formatting string
\return see fprintf(FILE *, *const char*, ...)
*/
int Fd_Code_Writer::crc_printf(const char *format, ...) {
va_list args;
va_start(args, format);
@ -965,6 +1024,12 @@ int Fd_Code_Writer::crc_printf(const char *format, ...) {
return ret;
}
/** Write formatted text to the code file.
If MergeBack is enabled, the CRC calculation is continued.
\param[in] format printf style formatting string
\param[in] args list of arguments
\return see fprintf(FILE *, *const char*, ...)
*/
int Fd_Code_Writer::crc_vprintf(const char *format, va_list args) {
if (g_project.write_mergeback_data) {
int n = vsnprintf(block_buffer_, block_buffer_size_, format, args);
@ -981,6 +1046,11 @@ int Fd_Code_Writer::crc_vprintf(const char *format, va_list args) {
}
}
/** Write some text to the code file.
If MergeBack is enabled, the CRC calculation is continued.
\param[in] text any text, no requirements to end in a newline or such
\return see fputs(const char*, FILE*)
*/
int Fd_Code_Writer::crc_puts(const char *text) {
if (g_project.write_mergeback_data) {
crc_add(text);
@ -988,6 +1058,12 @@ int Fd_Code_Writer::crc_puts(const char *text) {
return fputs(text, code_file);
}
/** Write a single ASCII character to the code file.
If MergeBack is enabled, the CRC calculation is continued.
\note to wrote UTF8 characters, use Fd_Code_Writer::crc_puts(const char *text)
\param[in] c any character between 0 and 127 inclusive
\return see fputc(int, FILE*)
*/
int Fd_Code_Writer::crc_putc(int c) {
if (g_project.write_mergeback_data) {
uchar uc = (uchar)c;
@ -996,292 +1072,5 @@ int Fd_Code_Writer::crc_putc(int c) {
return fputc(c, code_file);
}
extern Fl_Window *the_panel;
/** Remove the first two spaces at every line start.
\param[inout] s block of C code
*/
static void unindent(char *s) {
char *d = s;
bool line_start = true;
while (*s) {
if (line_start) {
if (*s>0 && isspace(*s)) s++;
if (*s>0 && isspace(*s)) s++;
line_start = false;
}
if (*s=='\r') s++;
if (*s=='\n') line_start = true;
*d++ = *s++;
}
*d = 0;
}
static Fl_String unindent_block(FILE *f, long start, long end) {
long bsize = end-start;
long here = ::ftell(f);
::fseek(f, start, SEEK_SET);
char *block = (char*)::malloc(bsize+1);
size_t n = ::fread(block, bsize, 1, f);
if (n!=1)
block[0] = 0; // read error
else
block[bsize] = 0;
unindent(block);
Fl_String str = block;
::free(block);
::fseek(f, here, SEEK_SET);
return str;
}
// TODO: add level of mergeback support to user settings
// TODO: command line option for mergeback
// TODO: automatic mergeback when a new project is loaded
// TODO: automatic mergeback when FLUID becomes app in focus
// NOTE: automatic mergeback on timer when file changes if app focus doesn't work
// NOTE: we could also let the user edit comment blocks
/**
Merge external changes in a source code file back into the current project.
This experimental function reads a source code file line by line. When it
encounters a special tag in a line, the crc32 stored in the tag is compared
to the crc32 that was calculated from the code lines since the previous tag.
If the crc's differ, the user has modified the source file externally, and the
given block differs from the block as it was generated by FLUID. Depending on
the block type, the user has modified the widget code (FD_TAG_GENERIC), which
can not be transferred back into the project.
Modifications to code blocks and callbacks (CODE, CALLBACK) can be merged back
into the project. Their corresponding Fl_Type is found using the unique
node id that is part of the tag. The block is only merged back if the crc's
from the project and from the edited block differ.
The caller must make sure that this code file was generated by the currently
loaded project.
The user is informed in detailed dialogs what the function discovered and
offered to merge or cancel if appropriate. Just in case this function is
destructive, "undo" restores the state before a MergeBack.
Callers can set different task. FD_MERGEBACK_CHECK checks if there are any
modifications in the code file and returns -1 if there was an error, or a
bit field where bit 0 is set if internal structures were modified, bit 1 if
code was changed, and bit 2 if modified blocks were found, but no Type node.
Bit 3 is set, if code was changed in the code file *and* the project.
FD_MERGEBACK_INTERACTIVE checks for changes and presents a status dialog box
to the user if there were conflicting changes or if a mergeback is possible,
presenting the user the option to merge or cancel. Returns 0 if the project
remains unchanged, and 1 if the user merged changes back. -1 is returned if an
invalid tag was found.
FD_MERGEBACK_GO merges all changes back into the project without any
interaction. Returns 0 if nothing changed, and 1 if it merged any changes back.
FD_MERGEBACK_GO_SAFE merges changes back only if there are no conflicts.
Returns 0 if nothing changed, and 1 if it merged any changes back, and -1 if
there were conflicts.
\note this function is currently part of Fd_Code_Writer to get easy access
to our crc32 code that also wrote the code file originally.
\param[in] s path and filename of the source code file
\param[in] task see above
\return see above
*/
int Fd_Code_Writer::merge_back(const char *s, int task) {
// nothing to be done if the mergeback option is disabled in the project
if (!g_project.write_mergeback_data) return 0;
int ret = 0;
bool changed = false;
FILE *code = fl_fopen(s, "r");
if (!code) return 0;
int iter = 0;
for (iter = 0; ; ++iter) {
int line_no = 0;
long block_start = 0;
long block_end = 0;
int num_changed_code = 0;
int num_changed_structure = 0;
int num_uid_not_found = 0;
int num_possible_override = 0;
int tag_error = 0;
if (task==FD_MERGEBACK_GO)
undo_checkpoint();
// NOTE: if we can get the CRC from the current callback, and it's the same
// as the code file CRC, merging back is very safe.
block_crc_ = crc32(0, NULL, 0);
block_line_start_ = true;
::fseek(code, 0, SEEK_SET);
changed = false;
for (;;) {
char line[1024];
if (fgets(line, 1023, code)==0) break;
line_no++;
const char *tag = strstr(line, "//~fl~");
if (!tag) {
crc_add(line);
block_end = ::ftell(code);
} else {
int type = -1;
int uid = 0;
unsigned long crc = 0;
int n = sscanf(tag, "//~fl~%d~%04x~%08lx~~", &type, &uid, &crc);
if (n!=3 || type<0 || type>FD_TAG_LAST ) { tag_error = 1; break; }
if (block_crc_ != crc) {
if (task==FD_MERGEBACK_GO) {
if (type==FD_TAG_MENU_CALLBACK || type==FD_TAG_WIDGET_CALLBACK) {
Fl_Type *tp = Fl_Type::find_by_uid(uid);
if (tp && tp->is_true_widget()) {
Fl_String cb = tp->callback(); cb += "\n";
unsigned long proj_crc = block_crc(cb.c_str());
if (proj_crc!=block_crc_)
tp->callback(unindent_block(code, block_start, block_end).c_str());
changed = true;
}
} else if (type==FD_TAG_CODE) {
Fl_Type *tp = Fl_Type::find_by_uid(uid);
if (tp && tp->is_a(ID_Code)) {
Fl_String cb = tp->name(); cb += "\n";
unsigned long proj_crc = block_crc(cb.c_str());
if (proj_crc!=block_crc_)
tp->name(unindent_block(code, block_start, block_end).c_str());
changed = true;
}
}
} else {
if (type==FD_TAG_MENU_CALLBACK || type==FD_TAG_WIDGET_CALLBACK) {
Fl_Type *tp = Fl_Type::find_by_uid(uid);
if (tp && tp->is_true_widget()) {
Fl_String cb = tp->callback(); cb += "\n";
unsigned long proj_crc = block_crc(cb.c_str());
// check if the code and project crc are the same, so this modification was already applied
if (proj_crc!=block_crc_) {
num_changed_code++;
// check if the block change on the project side as well, so we may override changes
if (proj_crc!=crc) {
num_possible_override++;
}
}
} else {
num_uid_not_found++;
num_changed_code++;
}
} else if (type==FD_TAG_CODE) {
Fl_Type *tp = Fl_Type::find_by_uid(uid);
if (tp && tp->is_a(ID_Code)) {
Fl_String code = tp->name(); code += "\n";
unsigned long proj_crc = block_crc(code.c_str());
// check if the code and project crc are the same, so this modification was already applied
if (proj_crc!=block_crc_) {
num_changed_code++;
// check if the block change on the project side as well, so we may override changes
if (proj_crc!=crc) {
num_possible_override++;
}
}
} else {
num_changed_code++;
num_uid_not_found++;
}
} else if (type==FD_TAG_GENERIC) {
num_changed_structure++;
}
}
}
// reset everything for the next block
block_crc_ = crc32(0, NULL, 0);
block_line_start_ = true;
block_start = ::ftell(code);
}
}
if (task==FD_MERGEBACK_CHECK) {
if (tag_error) { ret = -1; break; }
if (num_changed_structure) ret |= 1;
if (num_changed_code) ret |= 2;
if (num_uid_not_found) ret |= 4;
if (num_possible_override) ret |= 8;
break;
} else if (task==FD_MERGEBACK_INTERACTIVE) {
if (tag_error) {
fl_message("MergeBack found an error in line %d while reading tags\n"
"from the source code. Merging code back is not possible.", line_no);
ret = -1;
break;
}
if (!num_changed_code && !num_changed_structure) {
ret = 0;
break;
}
if (num_changed_structure && !num_changed_code) {
fl_message("MergeBack found %d modifications in the project structure\n"
"of the source code. These kind of changes can no be\n"
"merged back and will be lost when the source code is\n"
"generated again from the open project.", num_changed_structure);
ret = -1;
break;
}
Fl_String msg = "MergeBack found %1$d modifications in the source code.";
if (num_possible_override)
msg += "\n\nWARNING: %4$d of these modified blocks appear to also have\n"
"changed in the project. Merging will override changes in\n"
"the project with changes from the source code file.";
if (num_uid_not_found)
msg += "\n\nWARNING: for %2$d of these modifications no Type node\n"
"can be found and these modification can't be merged back.";
if (!num_possible_override && !num_uid_not_found)
msg += "\nMerging these changes back appears to be safe.";
if (num_changed_structure)
msg += "\n\nWARNING: %3$d modifications were found in the project\n"
"structure. These kind of changes can no be merged back\n"
"and will be lost when the source code is generated again\n"
"from the open project.";
if (num_changed_code==num_uid_not_found) {
fl_message(msg.c_str(),
num_changed_code, num_uid_not_found,
num_changed_structure, num_possible_override);
ret = -1;
break;
} else {
msg += "\n\nClick Cancel to abort the MergeBack operation.\n"
"Click Merge to merge all code changes back into\n"
"the open project.";
int c = fl_choice(msg.c_str(), "Cancel", "Merge", NULL,
num_changed_code, num_uid_not_found,
num_changed_structure, num_possible_override);
if (c==0) { ret = 1; break; }
task = FD_MERGEBACK_GO;
continue;
}
} else if (task==FD_MERGEBACK_GO) {
if (changed) ret = 1;
break;
} else if (task==FD_MERGEBACK_GO_SAFE) {
if (tag_error || num_changed_structure || num_possible_override) {
ret = -1;
break;
}
if (num_changed_code==0) {
ret = 0;
break;
}
task = FD_MERGEBACK_GO;
continue;
}
}
fclose(code);
if (changed) {
set_modflag(1);
if (the_panel) propagate_load(the_panel, LOAD);
}
return ret;
}
/// \}

View File

@ -31,31 +31,32 @@ struct Fd_Pointer_Tree;
int is_id(char c);
int write_strings(const Fl_String &filename);
const int FD_TAG_GENERIC = 0;
const int FD_TAG_CODE = 1;
const int FD_TAG_MENU_CALLBACK = 2;
const int FD_TAG_WIDGET_CALLBACK = 3;
const int FD_TAG_LAST = 3;
const int FD_MERGEBACK_CHECK = 0;
const int FD_MERGEBACK_INTERACTIVE = 1;
const int FD_MERGEBACK_GO = 2;
const int FD_MERGEBACK_GO_SAFE = 3;
class Fd_Code_Writer
{
protected:
/// file pointer for the C++ code file
FILE *code_file;
/// file pointer for the C++ header file
FILE *header_file;
/// tree of unique but human-readable identifiers
Fd_Identifier_Tree* id_root;
/// searchable text tree for text that is only written once to the header file
Fd_Text_Tree *text_in_header;
/// searchable text tree for text that is only written once to the code file
Fd_Text_Tree *text_in_code;
/// searchable tree for pointers that are only written once to the code file
Fd_Pointer_Tree *ptr_in_code;
/// crc32 for blocks of text written to the code file
unsigned long block_crc_;
char *block_buffer_;
int block_buffer_size_;
/// if set, we are at the start of a line and can ignore leading spaces in crc
bool block_line_start_;
/// expanding buffer for vsnprintf
char *block_buffer_;
/// size of expanding buffer for vsnprintf
int block_buffer_size_;
void crc_add(const void *data, int n=-1);
int crc_printf(const char *format, ...);
int crc_vprintf(const char *format, va_list args);
@ -63,19 +64,25 @@ protected:
int crc_putc(int c);
public:
/// current level of source code indentation
int indentation;
/// set if we write abbreviated file for the source code previewer
/// (disables binary data blocks, for example)
bool write_sourceview;
// silly thing to prevent declaring unused variables:
// When this symbol is on, all attempts to write code don't write
// anything, but set a variable if it looks like the variable "o" is used:
/// silly thing to prevent declaring unused variables:
/// When this symbol is on, all attempts to write code don't write
/// anything, but set a variable if it looks like the variable "o" is used:
int varused_test;
/// set to 1 if varused_test found that a variable is actually used
int varused;
public:
Fd_Code_Writer();
~Fd_Code_Writer();
const char* unique_id(void* o, const char*, const char*, const char*);
/// Increment source code indentation level.
void indent_more() { indentation++; }
/// Decrement source code indentation level.
void indent_less() { indentation--; }
const char *indent();
const char *indent(int set);
@ -97,7 +104,6 @@ public:
void write_public(int state); // writes pubic:/private: as needed
void tag(int type, unsigned short uid);
int merge_back(const char *s, int task);
static unsigned long block_crc(const void *data, int n=-1, unsigned long in_crc=0, bool *inout_line_start=NULL);
};

View File

@ -27,6 +27,7 @@
#include "undo.h"
#include "file.h"
#include "code.h"
#include "mergeback.h"
#include "alignment_panel.h"
#include "function_panel.h"
@ -1284,13 +1285,9 @@ int mergeback_code_files()
return 0;
}
// -- generate the file names with absolute paths
Fd_Code_Writer f;
Fl_String code_filename = g_project.codefile_path() + g_project.codefile_name();
// -- write the code and header files
if (!batch_mode) enter_project_dir();
int c = f.merge_back(code_filename.c_str(), FD_MERGEBACK_INTERACTIVE);
int c = merge_back(code_filename, FD_MERGEBACK_INTERACTIVE);
if (!batch_mode) leave_project_dir();
if (c==0) fl_message("MergeBack found no external modifications\n"
"in the source code.");

476
fluid/mergeback.cxx Normal file
View File

@ -0,0 +1,476 @@
//
// Code output routines for the Fast Light Tool Kit (FLTK).
//
// Copyright 1998-2023 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file. If this
// file is missing or damaged, see the license at:
//
// https://www.fltk.org/COPYING.php
//
// Please see the following page on how to report bugs and issues:
//
// https://www.fltk.org/bugs.php
//
#include "mergeback.h"
#include "fluid.h"
#include "code.h"
#include "undo.h"
#include "Fl_Function_Type.h"
#include "Fl_Widget_Type.h"
#include <FL/Fl_Window.H>
#include <FL/fl_ask.H>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <zlib.h>
extern void propagate_load(Fl_Group*, void*);
extern void load_panel();
extern void redraw_browser();
// TODO: add application user setting to control mergeback
// [] new projects default to mergeback
// [] check mergeback when loading project
// [] check mergeback when app gets focus
// [] always apply if safe
// TODO: command line option for mergeback
// -mb or --merge-back
// -mbs or --merge-back-if-safe
// NOTE: automatic mergeback on timer when file changes if app focus doesn't work
// NOTE: allow the user to edit comment blocks
/**
Merge external changes in a source code file back into the current project.
This experimental function reads a source code file line by line. When it
encounters a special tag in a line, the crc32 stored in the tag is compared
to the crc32 that was calculated from the code lines since the previous tag.
If the crc's differ, the user has modified the source file externally, and the
given block differs from the block as it was generated by FLUID. Depending on
the block type, the user has modified the widget code (FD_TAG_GENERIC), which
can not be transferred back into the project.
Modifications to code blocks and callbacks (CODE, CALLBACK) can be merged back
into the project. Their corresponding Fl_Type is found using the unique
node id that is part of the tag. The block is only merged back if the crc's
from the project and from the edited block differ.
The caller must make sure that this code file was generated by the currently
loaded project.
The user is informed in detailed dialogs what the function discovered and
offered to merge or cancel if appropriate. Just in case this function is
destructive, "undo" restores the state before a MergeBack.
Callers can set different task. FD_MERGEBACK_ANALYSE checks if there are any
modifications in the code file and returns -1 if there was an error, or a
bit field where bit 0 is set if internal structures were modified, bit 1 if
code was changed, and bit 2 if modified blocks were found, but no Type node.
Bit 3 is set, if code was changed in the code file *and* the project.
FD_MERGEBACK_INTERACTIVE checks for changes and presents a status dialog box
to the user if there were conflicting changes or if a mergeback is possible,
presenting the user the option to merge or cancel. Returns 0 if the project
remains unchanged, and 1 if the user merged changes back. -1 is returned if an
invalid tag was found.
FD_MERGEBACK_APPLY merges all changes back into the project without any
interaction. Returns 0 if nothing changed, and 1 if it merged any changes back.
FD_MERGEBACK_APPLY_IF_SAFE merges changes back only if there are no conflicts.
Returns 0 if nothing changed, and 1 if it merged any changes back, and -1 if
there were conflicts.
\note this function is currently part of Fd_Code_Writer to get easy access
to our crc32 code that also wrote the code file originally.
\param[in] s path and filename of the source code file
\param[in] task see above
\return see above
*/
int merge_back(const Fl_String &s, int task) {
if (g_project.write_mergeback_data) {
Fd_Mergeback mergeback;
return mergeback.merge_back(s, task);
} else {
// nothing to be done if the mergeback option is disabled in the project
return 0;
}
}
/** Allocate and initialize MergeBack class. */
Fd_Mergeback::Fd_Mergeback() :
code(NULL),
line_no(0),
tag_error(0),
num_changed_code(0),
num_changed_structure(0),
num_uid_not_found(0),
num_possible_override(0)
{
}
/** Release allocated resources. */
Fd_Mergeback::~Fd_Mergeback()
{
if (code) ::fclose(code);
}
/** Remove the first two spaces at every line start.
\param[inout] s block of C code
*/
void Fd_Mergeback::unindent(char *s) {
char *d = s;
bool line_start = true;
while (*s) {
if (line_start) {
if (*s>0 && isspace(*s)) s++;
if (*s>0 && isspace(*s)) s++;
line_start = false;
}
if (*s=='\r') s++;
if (*s=='\n') line_start = true;
*d++ = *s++;
}
*d = 0;
}
/**
Read a block of text from the source file and remove the leading two spaces in every line.
\param[in] start start of the block within the file
\param[in] end end of text within the file
\return a string holding the text that was found in the file
*/
Fl_String Fd_Mergeback::read_and_unindent_block(long start, long end) {
long bsize = end-start;
long here = ::ftell(code);
::fseek(code, start, SEEK_SET);
char *block = (char*)::malloc(bsize+1);
size_t n = ::fread(block, bsize, 1, code);
if (n!=1)
block[0] = 0; // read error
else
block[bsize] = 0;
unindent(block);
Fl_String str = block;
::free(block);
::fseek(code, here, SEEK_SET);
return str;
}
/** Tell user the results of our MergeBack analysis and pop up a dialog to give
the user a choice to merge or cancel.
\return 1 if the user wants to merge (choice dialog was shown)
\return 0 if there is nothing to merge (no dialog was shown)
\return -1 if the user wants to cancel or an error occurred or an issue was presented
(message or choice dialog was shown)
*/
int Fd_Mergeback::ask_user_to_merge() {
if (tag_error) {
fl_message("MergeBack found an error in line %d while reading tags\n"
"from the source code. Merging code back is not possible.", line_no);
return -1;
}
if (!num_changed_code && !num_changed_structure) {
return 0;
}
if (num_changed_structure && !num_changed_code) {
fl_message("MergeBack found %d modifications in the project structure\n"
"of the source code. These kind of changes can no be\n"
"merged back and will be lost when the source code is\n"
"generated again from the open project.", num_changed_structure);
return -1;
}
Fl_String msg = "MergeBack found %1$d modifications in the source code.";
if (num_possible_override)
msg += "\n\nWARNING: %4$d of these modified blocks appear to also have\n"
"changed in the project. Merging will override changes in\n"
"the project with changes from the source code file.";
if (num_uid_not_found)
msg += "\n\nWARNING: for %2$d of these modifications no Type node\n"
"can be found and these modification can't be merged back.";
if (!num_possible_override && !num_uid_not_found)
msg += "\nMerging these changes back appears to be safe.";
if (num_changed_structure)
msg += "\n\nWARNING: %3$d modifications were found in the project\n"
"structure. These kind of changes can no be merged back\n"
"and will be lost when the source code is generated again\n"
"from the open project.";
if (num_changed_code==num_uid_not_found) {
fl_message(msg.c_str(),
num_changed_code, num_uid_not_found,
num_changed_structure, num_possible_override);
return -1;
} else {
msg += "\n\nClick Cancel to abort the MergeBack operation.\n"
"Click Merge to merge all code changes back into\n"
"the open project.";
int c = fl_choice(msg.c_str(), "Cancel", "Merge", NULL,
num_changed_code, num_uid_not_found,
num_changed_structure, num_possible_override);
if (c==0) return -1;
return 1;
}
}
/** Analyse the block and its corresponding widget callback.
Return findings in num_changed_code, num_changed_code, and num_uid_not_found.
*/
void Fd_Mergeback::analyse_callback(unsigned long code_crc, unsigned long tag_crc, int uid) {
Fl_Type *tp = Fl_Type::find_by_uid(uid);
if (tp && tp->is_true_widget()) {
Fl_String cb = tp->callback(); cb += "\n";
unsigned long proj_crc = Fd_Code_Writer::block_crc(cb.c_str());
// check if the code and project crc are the same, so this modification was already applied
if (proj_crc!=code_crc) {
num_changed_code++;
// check if the block change on the project side as well, so we may override changes
if (proj_crc!=tag_crc) {
num_possible_override++;
}
}
} else {
num_uid_not_found++;
num_changed_code++;
}
}
/** Analyse the block and its corresponding Code Type.
Return findings in num_changed_code, num_changed_code, and num_uid_not_found.
*/
void Fd_Mergeback::analyse_code(unsigned long code_crc, unsigned long tag_crc, int uid) {
Fl_Type *tp = Fl_Type::find_by_uid(uid);
if (tp && tp->is_a(ID_Code)) {
Fl_String code = tp->name(); code += "\n";
unsigned long proj_crc = Fd_Code_Writer::block_crc(code.c_str());
// check if the code and project crc are the same, so this modification was already applied
if (proj_crc!=code_crc) {
num_changed_code++;
// check if the block change on the project side as well, so we may override changes
if (proj_crc!=tag_crc) {
num_possible_override++;
}
}
} else {
num_changed_code++;
num_uid_not_found++;
}
}
/** Analyse the code file and return findings in class member variables.
The code file must be open for reading already.
* tag_error is set if a tag was found, but could not be read
* line_no returns the line where an error occured
* num_changed_code is set to the number of changed code blocks in the file.
Code changes can be merged back to the project.
* num_changed_structure is set to the number of structural changes.
Structural changes outside of code blocks can not be read back.
* num_uid_not_found number of blocks that were modified, but the corresponding
type or widget can not be found in the project
* num_possible_override number of blocks that were changed in the code file,
but also were changed in the project.
\return -1 if reading a tag failed, otherwise 0
*/
int Fd_Mergeback::analyse() {
// initialize local variables
unsigned long code_crc = 0;
bool line_start = true;
char line[1024];
// bail if the caller has not opened a file yet
if (!code) return 0;
// initialize member variables to return our findings
line_no = 0;
tag_error = 0;
num_changed_code = 0;
num_changed_structure = 0;
num_uid_not_found = 0;
num_possible_override = 0;
code_crc = 0;
// loop through all lines in the code file
::fseek(code, 0, SEEK_SET);
for (;;) {
// get the next line until end of file
if (fgets(line, 1023, code)==0) break;
line_no++;
const char *tag = strstr(line, "//~fl~");
if (!tag) {
// if this line has no tag, add the contents to the CRC and continue
code_crc = Fd_Code_Writer::block_crc(line, -1, code_crc, &line_start);
} else {
// if this line has a tag, read all tag data
int tag_type = -1, uid = 0;
unsigned long tag_crc = 0;
int n = sscanf(tag, "//~fl~%d~%04x~%08lx~~", &tag_type, &uid, &tag_crc);
if (n!=3 || tag_type<0 || tag_type>FD_TAG_LAST ) { tag_error = 1; return -1; }
if (code_crc != tag_crc) {
switch (tag_type) {
case FD_TAG_GENERIC:
num_changed_structure++;
break;
case FD_TAG_MENU_CALLBACK:
case FD_TAG_WIDGET_CALLBACK:
analyse_callback(code_crc, tag_crc, uid);
break;
case FD_TAG_CODE:
analyse_code(code_crc, tag_crc, uid);
break;
}
}
// reset everything for the next block
code_crc = 0;
line_start = true;
}
}
return 0;
}
/** Apply callback mergebacks from the code file to the project.
\return 1 if the project changed
*/
int Fd_Mergeback::apply_callback(long block_end, long block_start, unsigned long code_crc, int uid) {
Fl_Type *tp = Fl_Type::find_by_uid(uid);
if (tp && tp->is_true_widget()) {
Fl_String cb = tp->callback(); cb += "\n";
unsigned long project_crc = Fd_Code_Writer::block_crc(cb.c_str());
if (project_crc!=code_crc) {
tp->callback(read_and_unindent_block(block_start, block_end).c_str());
return 1;
}
}
return 0;
}
/** Apply callback mergebacks from the code file to the project.
\return 1 if the project changed
*/
int Fd_Mergeback::apply_code(long block_end, long block_start, unsigned long code_crc, int uid) {
Fl_Type *tp = Fl_Type::find_by_uid(uid);
if (tp && tp->is_a(ID_Code)) {
Fl_String cb = tp->name(); cb += "\n";
unsigned long project_crc = Fd_Code_Writer::block_crc(cb.c_str());
if (project_crc!=code_crc) {
tp->name(read_and_unindent_block(block_start, block_end).c_str());
return 1;
}
}
return 0;
}
/** Apply all possible mergebacks from the code file to the project.
The code file must be open for reading already.
\return -1 if reading a tag failed, 0 if nothing changed, 1 if the project changed
*/
int Fd_Mergeback::apply() {
// initialize local variables
unsigned long code_crc = 0;
bool line_start = true;
char line[1024];
int changed = 0;
long block_start = 0;
long block_end = 0;
// bail if the caller has not opened a file yet
if (!code) return 0;
// initialize member variables to return our findings
line_no = 0;
tag_error = 0;
code_crc = 0;
// loop through all lines in the code file
::fseek(code, 0, SEEK_SET);
for (;;) {
// get the next line until end of file
if (fgets(line, 1023, code)==0) break;
line_no++;
const char *tag = strstr(line, "//~fl~");
if (!tag) {
// if this line has no tag, add the contents to the CRC and continue
code_crc = Fd_Code_Writer::block_crc(line, -1, code_crc, &line_start);
block_end = ::ftell(code);
} else {
// if this line has a tag, read all tag data
int tag_type = -1, uid = 0;
unsigned long tag_crc = 0;
int n = sscanf(tag, "//~fl~%d~%04x~%08lx~~", &tag_type, &uid, &tag_crc);
if (n!=3 || tag_type<0 || tag_type>FD_TAG_LAST ) { tag_error = 1; return -1; }
if (code_crc != tag_crc) {
if (tag_type==FD_TAG_MENU_CALLBACK || tag_type==FD_TAG_WIDGET_CALLBACK) {
changed |= apply_callback(block_end, block_start, code_crc, uid);
} else if (tag_type==FD_TAG_CODE) {
changed |= apply_code(block_end, block_start, code_crc, uid);
}
}
// reset everything for the next block
code_crc = 0;
line_start = true;
block_start = ::ftell(code);
}
}
return changed;
}
/** Dispatch the MergeBack into analysis, interactive, or apply directly.
\param[in] s source code filename and path
\param[in] task one of FD_MERGEBACK_ANALYSE, FD_MERGEBACK_INTERACTIVE,
FD_MERGEBACK_APPLY_IF_SAFE, or FD_MERGEBACK_APPLY
\return see ::merge_back(const Fl_String &s, int task)
*/
int Fd_Mergeback::merge_back(const Fl_String &s, int task) {
int ret = 0;
code = fl_fopen(s.c_str(), "r");
if (!code) return -1;
do { // no actual loop, just make sure we close the code file
if (task == FD_MERGEBACK_ANALYSE) {
analyse();
if (tag_error) {ret = -1; break; }
if (num_changed_structure) ret |= 1;
if (num_changed_code) ret |= 2;
if (num_uid_not_found) ret |= 4;
if (num_possible_override) ret |= 8;
break;
}
if (task == FD_MERGEBACK_INTERACTIVE) {
analyse();
ret = ask_user_to_merge();
if (ret != 1)
return ret;
task = FD_MERGEBACK_APPLY; // fall through
}
if (task == FD_MERGEBACK_APPLY_IF_SAFE) {
analyse();
if (tag_error || num_changed_structure || num_possible_override) {
ret = -1;
break;
}
if (num_changed_code==0) {
ret = 0;
break;
}
task = FD_MERGEBACK_APPLY; // fall through
}
if (task == FD_MERGEBACK_APPLY) {
ret = apply();
if (ret == 1) {
set_modflag(1);
redraw_browser();
load_panel();
}
ret = 1; // avoid message box in caller
}
} while (0);
fclose(code);
code = NULL;
return ret;
}

77
fluid/mergeback.h Normal file
View File

@ -0,0 +1,77 @@
//
// MergeBack routines for the Fast Light Tool Kit (FLTK).
//
// Copyright 2023 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file. If this
// file is missing or damaged, see the license at:
//
// https://www.fltk.org/COPYING.php
//
// Please see the following page on how to report bugs and issues:
//
// https://www.fltk.org/bugs.php
//
#ifndef _FLUID_MERGEBACK_H
#define _FLUID_MERGEBACK_H
#include <FL/fl_attr.h>
#include "../src/Fl_String.H"
#include <stdio.h>
const int FD_TAG_GENERIC = 0;
const int FD_TAG_CODE = 1;
const int FD_TAG_MENU_CALLBACK = 2;
const int FD_TAG_WIDGET_CALLBACK = 3;
const int FD_TAG_LAST = 3;
const int FD_MERGEBACK_ANALYSE = 0;
const int FD_MERGEBACK_INTERACTIVE = 1;
const int FD_MERGEBACK_APPLY = 2;
const int FD_MERGEBACK_APPLY_IF_SAFE = 3;
/** Class that implements the MergeBack functionality.
\see merge_back(const Fl_String &s, int task)
*/
class Fd_Mergeback
{
protected:
/// Pointer to the C++ code file.
FILE *code;
/// Current line number in the C++ code file.
int line_no;
/// Set if there was an error reading a tag.
int tag_error;
/// Number of code blocks that were different than the CRC in their tag.
int num_changed_code;
/// Number of generic structure blocks that were different than the CRC in their tag.
int num_changed_structure;
/// Number of code block that were modified, but a type node by that uid was not found.
int num_uid_not_found;
/// Number of modified code block where the corresponding project block also changed.
int num_possible_override;
void unindent(char *s);
Fl_String read_and_unindent_block(long start, long end);
void analyse_callback(unsigned long code_crc, unsigned long tag_crc, int uid);
void analyse_code(unsigned long code_crc, unsigned long tag_crc, int uid);
int apply_callback(long block_end, long block_start, unsigned long code_crc, int uid);
int apply_code(long block_end, long block_start, unsigned long code_crc, int uid);
public:
Fd_Mergeback();
~Fd_Mergeback();
int merge_back(const Fl_String &s, int task);
int ask_user_to_merge();
int analyse();
int apply();
};
extern int merge_back(const Fl_String &s, int task);
#endif // _FLUID_MERGEBACK_H