[geany/geany-plugins] 648224: scope: Improved evaluation on-hover

LarsGit223 git-noreply at xxxxx
Thu Jul 11 17:37:44 UTC 2019


Branch:      refs/heads/master
Author:      LarsGit223 <lars_paulsen at web.de>
Committer:   LarsGit223 <lars_paulsen at web.de>
Date:        Wed, 08 May 2019 19:27:32 UTC
Commit:      648224ab17c938db374bdba5a0bfc5dfae5c1033
             https://github.com/geany/geany-plugins/commit/648224ab17c938db374bdba5a0bfc5dfae5c1033

Log Message:
-----------
scope: Improved evaluation on-hover

Scope automatically evaluates an expression if the mouse pointer is hovered over it.
But before this change it only evaluated words. E.g. if the mouse was hovered over
"structa->item1" then scope would have evaluated "structa" or "item1" depending on
the exact position of the mouse pointer. With this change it will instead evaluate
the value of "structa" if the mouse pointer is over "structa". If the mouse pointer
is over "item1" then the expression "structa->item1" will be evaluated.


Modified Paths:
--------------
    scope/src/Makefile.am
    scope/src/tests/unittests.c
    scope/src/tests/utils_test.c
    scope/src/tooltip.c
    scope/src/utils.c
    scope/src/utils.h

Modified: scope/src/Makefile.am
14 lines changed, 14 insertions(+), 0 deletions(-)
===================================================================
@@ -61,3 +61,17 @@ scope_la_CFLAGS = $(AM_CFLAGS) $(VTE_CFLAGS) \
 	-I$(top_srcdir)/utils/src
 
 include $(top_srcdir)/build/cppcheck.mk
+
+if UNITTESTS
+
+TESTS = unittests
+check_PROGRAMS = unittests
+
+unittests_SOURCES  = tests/unittests.c tests/utils_test.c \
+                     $(scope_la_SOURCES)
+unittests_CPPFLAGS = -DTEST $(scope_la_CPPFLAGS)
+unittests_CFLAGS  = $(GEANY_CFLAGS) -DUNITTESTS $(scope_la_CFLAGS)
+unittests_LDADD    = @GEANY_LIBS@ $(INTLLIBS) @CHECK_LIBS@ \
+                     $(scope_la_LIBADD)
+
+endif


Modified: scope/src/tests/unittests.c
28 lines changed, 28 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,28 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <check.h>
+
+#include <gtk/gtk.h>
+#include "geany.h"
+
+TCase *utils_create_tests(void);
+
+Suite *
+my_suite(void)
+{
+	Suite *s = suite_create("Scope");
+	suite_add_tcase(s, utils_create_tests());
+	return s;
+}
+
+int
+main(void)
+{
+	int nf;
+	Suite *s = my_suite();
+	SRunner *sr = srunner_create(s);
+	srunner_run_all(sr, CK_NORMAL);
+	nf = srunner_ntests_failed(sr);
+	srunner_free(sr);
+	return (nf == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
+}


Modified: scope/src/tests/utils_test.c
73 lines changed, 73 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,73 @@
+#include <check.h>
+#include "../src/common.h"
+#include "../src/utils.h"
+
+typedef struct S_EXPR_TEST_ITEM
+{
+	const gchar *input;
+	guint peek_index;
+	const gchar *expected;
+}EXPR_TEST_ITEM;
+
+EXPR_TEST_ITEM expr_test_items [] =
+{
+	{ " variable ",         sizeof(" var")-1,            "variable"           },
+	{ " (variable) ",       sizeof(" (var")-1,           "variable"           },
+	{ "struct.item",        sizeof("str")-1,             "struct"             },
+	{ "struct.item",        sizeof("str")-1,             "struct"             },
+	{ "struct.item",        sizeof("struct.it")-1,       "struct.item"        },
+	{ "struct->item",       sizeof("str")-1,             "struct"             },
+	{ "struct->item",       sizeof("struct->it")-1,      "struct->item"       },
+	{ "&(struct->item)",    sizeof("&(str")-1,           "struct"             },
+	{ "&(struct->item)",    sizeof("&(struct->it")-1,    "struct->item"       },
+	{ "foobar(item)",       sizeof("foobar(it")-1,       "item"               },
+	{ "sizeof(item)",       sizeof("sizeof(it")-1,       "sizeof(item)"       },
+	{ "array[5]",           sizeof("arr")-1,             "array"              },
+	{ "array[5]",           sizeof("array[")-1,          "array[5]"           },
+	{ "*pointer",           sizeof("*poi")-1,            "pointer"            },
+	{ "*pointer",           0,                           "*pointer"           },
+	{ "&variable",          sizeof("&var")-1,            "variable"           },
+	{ "&variable",          0,                           "&variable"          },
+	{ "foo variable",       sizeof("foo var")-1,         "variable"           },
+	{ "variable foo",       sizeof("var")-1,             "variable"           },
+	{ "int var_a, var_b;",  sizeof("int var")-1,         "var_a"              },
+	{ "int var_a, var_b;",  sizeof("int var_a, va")-1,   "var_b"              },
+	{ "foo(var_a, var_b);", sizeof("foo(var")-1,         "var_a"              },
+	{ "foo(var_a, var_b);", sizeof("foo(var_a, va")-1,   "var_b"              },
+	{ "array[index].item",  sizeof("array[index].i")-1,  "array[index].item"  },
+	{ "array[index]->item", sizeof("array[index]->i")-1, "array[index]->item" },
+	{ NULL,                 0,                         NULL           },
+};
+
+START_TEST(test_parser_for_evaluate)
+{
+	EXPR_TEST_ITEM *test_item;
+	guint index = 0;
+	gchar *expr, *chunk;
+
+	test_item = &(expr_test_items[index]);
+	while (test_item->input != NULL)
+	{
+		chunk = g_strdup(test_item->input);
+		expr = utils_evaluate_expr_from_string (chunk, test_item->peek_index);
+		ck_assert_ptr_ne(expr, NULL);
+		if (g_strcmp0(expr, test_item->expected) != 0)
+		{
+			ck_abort_msg("Index #%lu: Input: '%s', Peek-Ind.: %lu, Result: '%s' != '%s' (expected)",
+				index, test_item->input, test_item->peek_index, expr, test_item->expected);
+		}
+		g_free(chunk);
+		g_free(expr);
+
+		index++;
+		test_item = &(expr_test_items[index]);
+	}
+}
+END_TEST;
+
+TCase *utils_create_tests(void)
+{
+	TCase *tc_utils = tcase_create("utils");
+	tcase_add_test(tc_utils, test_parser_for_evaluate);
+	return tc_utils;
+}


Modified: scope/src/tooltip.c
40 lines changed, 34 insertions(+), 6 deletions(-)
===================================================================
@@ -22,6 +22,9 @@
 
 #include "common.h"
 
+/* Wordchars defining possible signs in an expression. */
+#define SCOPE_EXPR_WORDCHARS	"_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.->[]"
+
 static void tooltip_trigger(void)
 {
 	GdkDisplay *display = gdk_display_get_default();
@@ -48,6 +51,7 @@ static void tooltip_trigger(void)
 	}
 }
 
+static gchar *last_expr = NULL;
 static gchar *output = NULL;
 static gint last_pos = -1;
 static gint peek_pos = -1;
@@ -69,6 +73,24 @@ static void tooltip_set(gchar *text)
 	}
 }
 
+static void tooltip_set_expr(gchar *expr, gchar *text)
+{
+	show = text != NULL;
+	g_free(output);
+	output = g_strdup_printf ("%s =\n %s", expr, text);
+	g_free(text);
+	g_free(expr);
+	last_pos = peek_pos;
+
+	if (show)
+	{
+		if (pref_tooltips_length && strlen(output) > (size_t) pref_tooltips_length + 3)
+			strcpy(output + pref_tooltips_length, "...");
+
+		tooltip_trigger();
+	}
+}
+
 static gint scid_gen = 0;
 
 void on_tooltip_error(GArray *nodes)
@@ -92,7 +114,7 @@ void on_tooltip_value(GArray *nodes)
 {
 	if (atoi(parse_grab_token(nodes)) == scid_gen)
 	{
-		tooltip_set(parse_get_display_from_7bit(parse_lead_value(nodes),
+		tooltip_set_expr(last_expr, parse_get_display_from_7bit(parse_lead_value(nodes),
 			parse_mode_get(input, MODE_HBIT), parse_mode_get(input, MODE_MEMBER)));
 	}
 }
@@ -107,17 +129,23 @@ static gboolean tooltip_launch(gpointer gdata)
 		(debug_state() & DS_SENDABLE))
 	{
 		ScintillaObject *sci = doc->editor->sci;
-		gchar *expr = sci_get_selection_mode(sci) == SC_SEL_STREAM &&
+		gchar *expr;
+		if (sci_get_selection_mode(sci) == SC_SEL_STREAM &&
 			peek_pos >= sci_get_selection_start(sci) &&
-			peek_pos < sci_get_selection_end(sci) ?
-			editor_get_default_selection(doc->editor, FALSE, NULL) :
-			editor_get_word_at_pos(doc->editor, peek_pos, NULL);
+			peek_pos < sci_get_selection_end(sci))
+		{
+			expr = editor_get_default_selection(doc->editor, FALSE, NULL);
+		}
+		else
+		{
+			expr = utils_read_evaluate_expr(doc->editor, peek_pos);
+		}
 
 		if ((expr = utils_verify_selection(expr)) != NULL)
 		{
 			g_free(input);
 			input = debug_send_evaluate('3', scid_gen, expr);
-			g_free(expr);
+			last_expr = expr;
 		}
 		else
 			tooltip_set(NULL);


Modified: scope/src/utils.c
347 lines changed, 347 insertions(+), 0 deletions(-)
===================================================================
@@ -36,6 +36,10 @@
 #ifdef G_OS_UNIX
 #include <fcntl.h>
 
+/* The maximum length of an expression for evaluating the value.
+   (Including the string terminator '\0') */
+#define SCOPE_MAX_EVALUATE_EXPR_LENGTH 256
+
 void show_errno(const char *prefix)
 {
 	show_error(_("%s: %s."), prefix, g_strerror(errno));
@@ -753,3 +757,346 @@ void utils_finalize(void)
 			utils_unlock(documents[i]);
 	}
 }
+
+/* checks whether @p c is an ASCII character (e.g. < 0x80) */
+#define IS_ASCII(c) (((unsigned char)(c)) < 0x80)
+
+/* This function is doing the parsing for 'utils_read_evaluate_expr()'.
+   It was mainly separated from it to support easy unit testing. */
+gchar *utils_evaluate_expr_from_string(gchar *chunk, guint peek_index)
+{
+	static const gchar *keep_prefix_list[] =
+		{ "sizeof", NULL };
+	static gchar expr[SCOPE_MAX_EVALUATE_EXPR_LENGTH];
+	static const gchar *wchars = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+	static const gchar *wspace = " \t";
+	gint startword, endword, temp_pos, round_brackets;
+	gint pos, left_bracket, cmp;
+	gboolean stop, oper, whitespace, brackets_exist;
+	gchar *expr_copy;
+
+	g_return_val_if_fail(chunk != NULL, NULL);
+
+	startword = peek_index;
+	endword = peek_index;
+	expr[0] = '\0';
+
+	if (chunk[startword] == ' ')
+	{
+		return NULL;
+	}
+
+	/* Find the left boundary of the expression. If the current sign
+	   is a '*' or a '&' then we already are at the left boundary. */
+	round_brackets = 0;
+	if (chunk[startword] != '*' && chunk[startword] != '&')
+	{
+		stop = FALSE;
+		oper = FALSE;
+		whitespace = FALSE;
+		while (startword > 0 && stop == FALSE)
+		{
+			/* the checks for "c < 0" are to allow any Unicode character which should make the code
+			 * a little bit more Unicode safe, anyway, this allows also any Unicode punctuation */
+			if (strchr(wchars, chunk[startword]) || ! IS_ASCII(chunk[startword]))
+			{
+				if (whitespace == TRUE && oper == FALSE)
+				{
+					startword++;
+					stop = TRUE;
+				}
+				else
+				{
+					startword--;
+					oper = FALSE;
+					whitespace = FALSE;
+				}
+			}
+			else
+			{
+				switch (chunk[startword])
+				{
+					case ' ':
+					case '\t':
+						startword--;
+						whitespace = TRUE;
+					break;
+
+					case '(':
+						round_brackets++;
+						startword--;
+					break;
+
+					case ')':
+						round_brackets--;
+						startword--;
+					break;
+
+					case '[':
+					case ']':
+						startword--;
+					break;
+
+					case '.':
+						/* Stop if there are no more signs before the current position,
+						   or if we already have seen an operator, or the sign before
+						   the operator is not a valid variable-name-char or whitespace. */
+						if (startword == 0 || oper == TRUE ||
+							(strchr(wchars, chunk[startword - 1]) == NULL &&
+							 strchr(wspace, chunk[startword - 1]) == NULL &&
+							 chunk[startword - 1] != ']'))
+						{
+							stop = TRUE;
+							break;
+						}
+						oper = TRUE;
+						startword--;
+					break;
+
+					case '>':
+						/* Stop if there are no more signs before the current position,
+						   or if we already have seen an operator, or the sign before
+						   the current sign is not a '-' (we expect a "->" here) */
+						if (startword < 2 || oper == TRUE || chunk[startword - 1] != '-')
+						{
+							stop = TRUE;
+							break;
+						}
+						oper = TRUE;
+						startword -= 2;
+					break;
+
+					case ':':
+						/* Stop if there are no more signs before the current position,
+						   or if we already have seen an operator, or the sign before
+						   the current sign is not a ':' (we expect a "::" here) */
+						if (startword < 2 || oper == TRUE || chunk[startword - 1] != ':')
+						{
+							stop = TRUE;
+							break;
+						}
+						oper = TRUE;
+						startword -= 2;
+					break;
+
+					default:
+						/* Not a valid variable-name-char or whitespace or operator. */
+						stop = TRUE;
+						if (whitespace == TRUE)
+						{
+							startword += 2;
+						}
+					break;
+				}
+			}
+		}
+		if (chunk[startword] == '*' || chunk[startword] == '&')
+		{
+			startword++;
+		}
+	}
+
+	/* Find the right boundary of the expression. */
+	stop = FALSE;
+	while (chunk[endword] != 0 && stop == FALSE)
+	{
+		/* the checks for "c < 0" are to allow any Unicode character which should make the code
+		 * a little bit more Unicode safe, anyway, this allows also any Unicode punctuation */
+		if (strchr(wchars, chunk[endword]) || ! IS_ASCII(chunk[endword]))
+		{
+			endword++;
+		}
+		else
+		{
+			switch (chunk[endword])
+			{
+				case ')':
+					round_brackets--;
+					if (round_brackets < 1)
+					{
+						/* We stop here in any case. 0 would be the normal case.
+						   If the value is below zero something went wrong in the
+						   loop above. Maybe this is just not a valid expression. */
+						stop = TRUE;
+					}
+				break;
+
+				case ']':
+				case '*':
+				case '&':
+					endword++;
+				break;
+
+				case ' ':
+				case '\t':
+					/* We should usually stop here. But we need to continue
+					   if the whitespace is followed by an array index "[...]"! */
+					temp_pos = endword + 1;
+					while (chunk[temp_pos] != 0 &&
+						   (chunk[temp_pos] == ' ' || chunk[temp_pos] == '\t'))
+					{
+						temp_pos++;
+					}
+					if (chunk[temp_pos] == '[' && chunk[temp_pos+1] != ']')
+					{
+						endword = temp_pos + 1;
+					}
+					else
+					{
+						stop = TRUE;
+					}
+				break;
+
+				default:
+					endword--;
+					stop = TRUE;
+				break;
+			}
+		}
+	}
+
+	/* Skip leading whitespace. */
+	while ((chunk[startword] == ' ' || chunk[startword] == '\t') &&
+		   startword < endword)
+	{
+		startword++;
+	}
+
+	/* Skip trailing whitespace. */
+	while (endword > 0 &&
+		   (chunk[endword] == ' ' || chunk[endword] == '\t') &&
+		   startword < endword)
+	{
+		endword--;
+	}
+
+	/* Validate/ensure balanced round brackets. */
+	round_brackets = 0;
+	left_bracket = 0;
+	stop = FALSE;
+	brackets_exist = FALSE;
+	for (pos = startword ; pos <= endword && stop == FALSE ; pos++)
+	{
+		switch (chunk[pos])
+		{
+			case '(':
+				round_brackets++;
+				brackets_exist = TRUE;
+				left_bracket = pos;
+			break;
+
+			case ')':
+				round_brackets--;
+				if (round_brackets == 0)
+				{
+					endword = pos;
+					stop = TRUE;
+				}
+				else if (round_brackets < 0)
+				{
+					endword = pos - 1;
+					stop = TRUE;
+				}
+			break;
+
+			case ' ':
+			case '\t':
+			break;
+
+			default:
+				temp_pos = pos;
+			break;
+		}
+	}
+
+	/* If brackets exist and the left-most sign is not a bracket itself,
+	   then it could be a function name or an operator (sizeof). We
+	   want to exclude the function name but include an operator name. */
+	if (brackets_exist == TRUE && chunk[startword] != '(')
+	{
+		pos = 0;
+		oper = FALSE;
+		while (keep_prefix_list[pos] != NULL)
+		{
+			cmp = strncmp(keep_prefix_list[pos], &(chunk[startword]),
+				strlen(keep_prefix_list[pos]));
+			if (cmp == 0)
+			{
+				/* Match with allowed operator/prefix. Keep it. */
+				oper = TRUE;
+				break;
+			}
+			pos++;
+		}
+
+		/* Did we find a wanted prefix? */
+		if (oper == FALSE)
+		{
+			/* No! Move startword back to position of left-most valid
+			   round bracket. */
+			startword = left_bracket;
+		}
+	}
+
+	/* Skip useless surrounding brackets if present. */
+	if (startword < endword)
+	{
+		guint skipped = 0;
+
+		while (chunk[startword] == '(' && startword < endword)
+		{
+			skipped++;
+			startword++;
+		}
+		while (chunk[endword] == ')' && skipped > 0)
+		{
+			skipped--;
+			endword--;
+		}
+	}
+
+	expr_copy = NULL;
+	if (startword < endword)
+	{
+		if (chunk[endword] != '\0')
+		{
+			chunk[endword+1] = '\0';
+		}
+
+		/* ensure null terminated */
+		g_strlcpy(expr, &(chunk[startword]), sizeof(expr));
+		expr_copy = g_strdup(expr);
+	}
+
+	return expr_copy;
+}
+
+/* Reads the expression for evaluate at the given cursor position and writes
+ * it into the given buffer. The buffer will be NULL terminated in any case,
+ * even when the word is truncated because wordlen is too small.
+ * Position can be -1, then the current position is used. */
+gchar *utils_read_evaluate_expr(GeanyEditor *editor, gint peek_pos)
+{
+	gint line, line_start;
+	gchar *chunk, *expr;
+	ScintillaObject *sci;
+
+	g_return_val_if_fail(editor != NULL, NULL);
+	sci = editor->sci;
+
+	if (peek_pos == -1)
+	{
+		peek_pos = sci_get_current_position(sci);
+	}
+
+	line = sci_get_line_from_position(sci, peek_pos);
+	line_start = sci_get_position_from_line(sci, line);
+	chunk = sci_get_line(sci, line);
+
+	/* Now that we got the content, let's parse it. */
+	expr = utils_evaluate_expr_from_string (chunk, peek_pos - line_start);
+
+	g_free(chunk);
+
+	return expr;
+}


Modified: scope/src/utils.h
3 lines changed, 3 insertions(+), 0 deletions(-)
===================================================================
@@ -107,5 +107,8 @@ void utils_tree_set_cursor(GtkTreeSelection *selection, GtkTreeIter *iter, gdoub
 void utils_init(void);
 void utils_finalize(void);
 
+gchar *utils_evaluate_expr_from_string(gchar *chunk, guint peek_index);
+gchar *utils_read_evaluate_expr(GeanyEditor *editor, gint pos);
+
 #define UTILS_H 1
 #endif



--------------
This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).


More information about the Plugins-Commits mailing list