1 /* Bluefish HTML Editor
2  * bftextview2_autocomp.c
3  *
4  * Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015 Olivier Sessink
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include <string.h>
21 #include "bluefish.h"
22 #include "bftextview2_private.h"
23 #include "bftextview2_scanner.h"
24 #include "bftextview2_identifier.h"
25 #include "bftextview2_autocomp.h"
26 #include "stringlist.h"
27 
28 /*#undef DBG_AUTOCOMP
29 #define DBG_AUTOCOMP g_print*/
30 
31 typedef struct {
32 	BluefishTextView *btv;
33 	gchar *prefix;
34 	gchar *newprefix;
35 	GtkWidget *win;
36 	GtkListStore *store;
37 	GtkTreeView *tree;
38 	GtkWidget *scroll;
39 	GtkWidget *reflabel;
40 	GtkCellRenderer *cell;
41 	GtkTreeViewColumn *column;
42 	gint listwidth;
43 	gint w;
44 	gint h;
45 	gboolean in_fill; /* TRUE while filling the liststore */
46 	guint16 contextnum;
47 } Tacwin;
48 
49 #define ACWIN(p) ((Tacwin *)(p))
50 
51 static void
acwin_cleanup(BluefishTextView * btv)52 acwin_cleanup(BluefishTextView * btv)
53 {
54 	if (btv->autocomp) {
55 		g_free(ACWIN(btv->autocomp)->prefix);
56 		gtk_widget_destroy(ACWIN(btv->autocomp)->win);
57 		g_slice_free(Tacwin, btv->autocomp);
58 		btv->autocomp = NULL;
59 	}
60 }
61 
62 static gboolean
acwin_move_selection(BluefishTextView * btv,gint keyval)63 acwin_move_selection(BluefishTextView * btv, gint keyval)
64 {
65 	GtkTreeSelection *selection;
66 	GtkTreeIter it;
67 	GtkTreeModel *model;
68 	GtkTreePath *path;
69 
70 	selection = gtk_tree_view_get_selection(ACWIN(btv->autocomp)->tree);
71 	if (gtk_tree_selection_get_selected(selection, &model, &it)) {
72 		gboolean retval = TRUE;
73 		gint i, rows = 12, *indices = NULL;
74 		path = gtk_tree_model_get_path(model, &it);
75 		indices = gtk_tree_path_get_indices(path);
76 		switch (keyval) {
77 		case GDK_Up:			/* move the selection one up */
78 			retval = gtk_tree_path_prev(path);
79 			break;
80 		case GDK_Down:
81 			gtk_tree_path_next(path);
82 			break;
83 		case GDK_Page_Down:
84 			i = MIN(gtk_tree_model_iter_n_children(model, NULL) - 1, indices[0] + rows);
85 			gtk_tree_path_free(path);
86 			path = gtk_tree_path_new_from_indices(i, -1);
87 			break;
88 		case GDK_Page_Up:
89 			i = MAX(indices[0] - rows, 0);
90 			gtk_tree_path_free(path);
91 			path = gtk_tree_path_new_from_indices(i, -1);
92 			break;
93 		case GDK_Home:
94 			gtk_tree_path_free(path);
95 			path = gtk_tree_path_new_first();
96 			break;
97 		case GDK_End:
98 			gtk_tree_path_free(path);
99 			i = gtk_tree_model_iter_n_children(model, NULL);
100 			path = gtk_tree_path_new_from_indices(i - 1, -1);
101 			break;
102 		default:
103 			return FALSE;
104 			break;
105 		}
106 		if (gtk_tree_model_get_iter(model, &it, path)) {
107 			gtk_tree_selection_select_iter(selection, &it);
108 			gtk_tree_view_scroll_to_cell(ACWIN(btv->autocomp)->tree, path, NULL, FALSE, 0, 0);
109 		} else
110 			retval = FALSE;
111 		gtk_tree_path_free(path);
112 		return retval;
113 	} else {
114 		/* set selection */
115 
116 	}
117 	return FALSE;
118 }
119 
120 static gchar *
string_maintain_indenting(BluefishTextView * btv,gchar * string,gint * backward_cursor)121 string_maintain_indenting(BluefishTextView * btv, gchar *string, gint *backward_cursor)
122 {
123 	gchar *indentstring;
124 	gint i=0, len;
125 	GtkTextIter iter;
126 	GString *gstr;
127 	/*g_print("string_maintain_indenting, check %s for newlines\n",string);*/
128 	/* check if there are newlines in the string*/
129 	if (!string || string[0]=='\0' || !strchr(string, '\n')) {
130 		return string;
131 	}
132 	gtk_text_buffer_get_iter_at_mark(btv->buffer, &iter, gtk_text_buffer_get_insert(btv->buffer));
133 	indentstring = get_line_indenting(btv->buffer, &iter, FALSE);
134 	/*g_print("indentstring='%s' with len %d\n",indentstring,strlen(indentstring));*/
135 	if (!indentstring || indentstring[0]=='\0') {
136 		return string;
137 	}
138 	/* now do the replace of \n with \n+indentstring */
139 	len = strlen(string);
140 	gstr = g_string_new("");
141 	while(string[i] != '\0') {
142 		g_string_append_c(gstr, string[i]);
143 		if (string[i] == '\n') {
144 			g_string_append(gstr, indentstring);
145 			if (i>=(len-*backward_cursor)) {
146 				*backward_cursor += strlen(indentstring);
147 			}
148 		}
149 		i++;
150 	}
151 	g_free(string);
152 	string = gstr->str;
153 	g_string_free(gstr, FALSE);
154 	return string;
155 }
156 
157 /* returns the number of bytes
158 that are already present in the text.
159 
160 this is used to avoid for example inserting "return;" in a location where ';' is already the
161 character directly beyond the insert position.
162 */
163 static gint
get_existing_end_len(BluefishTextView * btv,const gchar * string,gint prefix_bytelen)164 get_existing_end_len(BluefishTextView * btv, const gchar *string, gint prefix_bytelen)
165 {
166 	gchar *tmp;
167 	GtkTextIter it1, it2;
168 	gint i,len;
169 	gint string_len = g_utf8_strlen(string, -1);
170 
171 	gtk_text_buffer_get_iter_at_mark(btv->buffer, &it1, gtk_text_buffer_get_insert(btv->buffer));
172 	it2 = it1;
173 	DBG_AUTOCOMP("get_existing_end_len, forward %d chars\n",string_len - prefix_bytelen);
174 	gtk_text_iter_forward_chars(&it2,string_len - prefix_bytelen);
175 	DBG_AUTOCOMP("get the text %d:%d\n",gtk_text_iter_get_offset(&it1),gtk_text_iter_get_offset(&it2));
176 	tmp = gtk_text_buffer_get_text(btv->buffer, &it1, &it2, TRUE);
177 	/*g_print("got tmp='%s'\n",tmp);*/
178 	len = strlen(tmp);
179 	i = len-1;
180 	do {
181 		DBG_AUTOCOMP("get_existing_end_len, compare %d characters of %s and %s\n",i,string+prefix_bytelen+len-i,tmp);
182 		if (strncmp(string+prefix_bytelen+len-i, tmp, i)==0) {
183 			DBG_AUTOCOMP("get_existing_end_len, found %d existing characters\n",i);
184 			g_free(tmp);
185 			return i;
186 		}
187 		i--;
188 	} while(i>0);
189 	g_free(tmp);
190 	DBG_AUTOCOMP("get_existing_end_len, found no existing characters\n");
191 	return 0;
192 }
193 
trigger_new_autocomp_idle(gpointer data)194 static gboolean trigger_new_autocomp_idle(gpointer data) {
195 	DBG_AUTOCOMP("trigger_new_autocomp_idle\n");
196 	autocomp_run(BLUEFISH_TEXT_VIEW(data), FALSE);
197 	return FALSE;
198 }
199 
200 gboolean
acwin_check_keypress(BluefishTextView * btv,GdkEventKey * event)201 acwin_check_keypress(BluefishTextView * btv, GdkEventKey * event)
202 {
203 	DBG_AUTOCOMP("got keyval %c\n", event->keyval);
204 	if ((event->state & GDK_CONTROL_MASK) || (event->state & GDK_MOD1_MASK)) {
205 		DBG_AUTOCOMP("a modifier is active (state=%d), don't handle the keypress\n", event->state);
206 		return FALSE;
207 	}
208 	switch (event->keyval) {
209 	case GDK_Return:{
210 			GtkTreeSelection *selection;
211 			GtkTreeIter it;
212 			GtkTreeModel *model;
213 			BluefishTextView *master = BLUEFISH_TEXT_VIEW(btv->master);
214 			selection = gtk_tree_view_get_selection(ACWIN(btv->autocomp)->tree);
215 			if (selection && gtk_tree_selection_get_selected(selection, &model, &it)) {
216 				gchar *string;
217 				gint stringlen;
218 				gint existing_len, prefix_len;
219 				guint pattern_id;
220 				gint backup_chars = 0;
221 				gboolean trigger_new_autocomp_popup=FALSE;
222 				gtk_tree_model_get(model, &it, 1, &string, -1);
223 				/*g_print("context %d has patternhash %p, string=%s\n",ACWIN(btv->autocomp)->contextnum, g_array_index(btv->bflang->st->contexts, Tcontext, ACWIN(btv->autocomp)->contextnum).patternhash, string); */
224 				if (g_array_index
225 					(master->bflang->st->contexts, Tcontext, ACWIN(btv->autocomp)->contextnum).patternhash) {
226 					/*g_print("looking in context %d patternhash for '%s'\n",ACWIN(btv->autocomp)->contextnum, string); */
227 					pattern_id =
228 						GPOINTER_TO_INT(g_hash_table_lookup
229 										(g_array_index
230 										 (master->bflang->st->contexts, Tcontext,
231 										  ACWIN(btv->autocomp)->contextnum).patternhash, string));
232 					DBG_AUTOCOMP("got pattern_id=%d\n", pattern_id);
233 					if (pattern_id) {
234 						GSList *tmpslist =
235 							g_array_index(master->bflang->st->matches, Tpattern, pattern_id).autocomp_items;
236 						/* a pattern MAY have multiple autocomplete items. This code is not efficient if in the future some
237 						   patterns would have many autocomplete items. I don't expect this, so I leave this as it is right now  */
238 						while (tmpslist) {
239 							Tpattern_autocomplete *pac = tmpslist->data;
240 							if (g_strcmp0(string, pac->autocomplete_string) == 0) {
241 								backup_chars = pac->autocomplete_backup_cursor;
242 								trigger_new_autocomp_popup = (pac->trigger_new_autocomp_popup==1);
243 							}
244 							tmpslist = g_slist_next(tmpslist);
245 						}
246 					}
247 				}
248 				stringlen = strlen(string);
249 				prefix_len = strlen(ACWIN(btv->autocomp)->prefix);
250 				existing_len = get_existing_end_len(btv, string, prefix_len);
251 
252 				DBG_AUTOCOMP("acwin_check_keypress: ENTER: insert %s\n",
253 							 string + prefix_len);
254 				string[stringlen - existing_len] = '\0';
255 				/* see if there are any \n characters in the autocompletion string, and add the current indenting to them */
256 				string = string_maintain_indenting(btv, string, &backup_chars);
257 				gtk_text_buffer_insert_at_cursor(btv->buffer, string + prefix_len, -1);
258 				if (backup_chars != 0) {
259 					GtkTextIter iter;
260 					gtk_text_buffer_get_iter_at_mark(btv->buffer, &iter, gtk_text_buffer_get_insert(btv->buffer));
261 					if (backup_chars-existing_len > 0 && gtk_text_iter_backward_chars(&iter, backup_chars-existing_len)) {
262 						DBG_AUTOCOMP("move cursor %d chars back!\n", backup_chars);
263 						gtk_text_buffer_place_cursor(btv->buffer, &iter);
264 					}
265 				}
266 				if (trigger_new_autocomp_popup) {
267 					g_idle_add(trigger_new_autocomp_idle, btv);
268 				}
269 				g_free(string);
270 			}
271 			acwin_cleanup(btv);
272 			return TRUE;
273 		}
274 		break;
275 	case GDK_Tab:
276 		if (ACWIN(btv->autocomp)->newprefix
277 			&& strlen(ACWIN(btv->autocomp)->newprefix) > strlen(ACWIN(btv->autocomp)->prefix)) {
278 			gtk_text_buffer_insert_at_cursor(gtk_text_view_get_buffer(GTK_TEXT_VIEW(btv)),
279 											 ACWIN(btv->autocomp)->newprefix +
280 											 strlen(ACWIN(btv->autocomp)->prefix), -1);
281 		}
282 		/* now we have to re-position the autocomplete window */
283 		autocomp_run(btv, FALSE);
284 		return TRUE;
285 		break;
286 	case GDK_Right:
287 	case GDK_KP_Right:
288 	case GDK_Left:
289 	case GDK_KP_Left:
290 		acwin_cleanup(btv);
291 		return FALSE;
292 		break;
293 	case GDK_Escape:
294 		acwin_cleanup(btv);
295 		return TRUE;
296 		break;
297 	case GDK_Up:
298 	case GDK_Down:
299 	case GDK_Page_Down:
300 	case GDK_Page_Up:
301 	case GDK_Home:
302 	case GDK_End:
303 		if (acwin_move_selection(btv, event->keyval))
304 			return TRUE;
305 		break;
306 	}
307 
308 	return FALSE;
309 }
310 
311 static void
acw_selection_changed_lcb(GtkTreeSelection * selection,Tacwin * acw)312 acw_selection_changed_lcb(GtkTreeSelection * selection, Tacwin * acw)
313 {
314 	GtkTreeModel *model;
315 	GtkTreeIter iter;
316 	BluefishTextView *master = BLUEFISH_TEXT_VIEW(acw->btv->master);
317 	DBG_AUTOCOMP("acw_selection_changed_lcb, in_fill=%d\n",acw->in_fill);
318 	if (acw->in_fill)
319 		return;
320 
321 	if (!g_array_index(master->bflang->st->contexts, Tcontext, acw->contextnum).patternhash
322 		|| !main_v->props.show_autocomp_reference)
323 		return;
324 
325 	if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
326 		gchar *key;
327 		gtk_tree_model_get(model, &iter, 1, &key, -1);
328 		if (key) {
329 			DBG_AUTOCOMP("lookup reference for %s\n",key);
330 			gint pattern_id =
331 				GPOINTER_TO_INT(g_hash_table_lookup
332 								(g_array_index
333 								 (master->bflang->st->contexts, Tcontext, acw->contextnum).patternhash, key));
334 			DBG_AUTOCOMP("got pattern_id %d for key %s\n",pattern_id, key);
335 			g_free(key);
336 			if (pattern_id && g_array_index(master->bflang->st->matches, Tpattern, pattern_id).reference) {
337 				GtkRequisition requisition;
338 				DBG_AUTOCOMP("acw_selection_changed_lcb, show %s\n",
339 							 g_array_index(master->bflang->st->matches, Tpattern, pattern_id).reference);
340 				gtk_label_set_markup(GTK_LABEL(acw->reflabel),
341 									 g_array_index(master->bflang->st->matches, Tpattern,
342 												   pattern_id).reference);
343 				gtk_widget_show(acw->reflabel);
344 #if GTK_CHECK_VERSION(3,0,0)
345 				gtk_widget_get_preferred_size(acw->reflabel, &requisition, NULL);
346 #else
347 				gtk_widget_size_request(acw->reflabel, &requisition);
348 #endif
349 				/*gtk_window_get_size(GTK_WINDOW(acw->win),&width,&height); */
350 				acw->w = acw->listwidth + requisition.width + 2;
351 				gtk_widget_set_size_request(acw->win, acw->w, (acw->h<350)?350:-1);
352 				return;
353 			}
354 		}
355 	}
356 	gtk_widget_hide(acw->reflabel);
357 	acw->w = acw->listwidth;
358 	gtk_widget_set_size_request(acw->win, acw->listwidth, -1);
359 }
360 
361 static Tacwin *
acwin_create(BluefishTextView * btv)362 acwin_create(BluefishTextView * btv)
363 {
364 	/*GtkCellRenderer *cell;*/
365 	GtkWidget *hbox;
366 	/* GtkWidget *vbar; */
367 	Tacwin *acw;
368 	GtkTreeSelection *selection;
369 
370 	acw = g_slice_new0(Tacwin);
371 	acw->btv = btv;
372 	acw->in_fill = TRUE;
373 	acw->win = gtk_window_new(GTK_WINDOW_POPUP);
374 /* We do not do any customized drawing in autocomplete window, as far as I can see, so probably we do not need to switch this on.
375 * On MacOSX this causes the labels of acw->reflabel to pile up on each other, see report by Keith Gunthardt on Facebook:
376 * https://www.facebook.com/photo.php?fbid=10152794950839751&set=p.10152794950839751&type=1
377 
378 	I (Olivier) think you are right Andrius, we can remove this call for all platforms, uncommenting it:
379 	gtk_widget_set_app_paintable(acw->win, TRUE);
380 */
381 	gtk_window_set_resizable(GTK_WINDOW(acw->win), FALSE);
382 	gtk_container_set_border_width(GTK_CONTAINER(acw->win), 1);
383 	gtk_window_set_decorated(GTK_WINDOW(acw->win), FALSE);
384 
385 	/* setting the type hint makes wayland *not* create a subsurface (I learned that on IRC), and we need a
386 	subsurface, otherwise the popup can't be moved. we also need to make the window transient for the parent
387 	for the wayland backend to create a subsurface */
388 	/*gtk_window_set_type_hint(GTK_WINDOW(acw->win), GDK_WINDOW_TYPE_HINT_POPUP_MENU);*/
389 	gtk_window_set_transient_for(GTK_WINDOW(acw->win), GTK_WINDOW(BFWIN(DOCUMENT(acw->btv->doc)->bfwin)->main_window));
390 
391 	acw->store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
392 	acw->tree = GTK_TREE_VIEW(gtk_tree_view_new_with_model(GTK_TREE_MODEL(acw->store)));
393 	g_object_unref(acw->store);
394 
395 	gtk_tree_view_set_headers_visible(acw->tree, FALSE);
396 	acw->scroll = gtk_scrolled_window_new(NULL, NULL);
397 	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(acw->scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
398 	acw->cell = gtk_cell_renderer_text_new();
399 	acw->column = gtk_tree_view_column_new_with_attributes("", acw->cell, "markup", 0, NULL);
400 	gtk_tree_view_append_column(GTK_TREE_VIEW(acw->tree), acw->column);
401 	gtk_tree_view_set_enable_search(GTK_TREE_VIEW(acw->tree), FALSE);
402 	selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(acw->tree));
403 	g_signal_connect(G_OBJECT(selection), "changed", G_CALLBACK(acw_selection_changed_lcb), acw);
404 
405 /*	gtk_tree_view_set_search_column(GTK_TREE_VIEW(acw->tree),1);
406 	gtk_tree_view_set_search_equal_func(GTK_TREE_VIEW(acw->tree),acwin_tree_search_lcb,prefix,NULL);*/
407 
408 	/*g_signal_connect_swapped(GTK_WINDOW(acw->win),"expose-event",G_CALLBACK(ac_paint),acw->win); */
409 	/*gtk_window_set_position (GTK_WINDOW(acw->win), GTK_WIN_POS_MOUSE); */
410 	gtk_container_add(GTK_CONTAINER(acw->scroll), GTK_WIDGET(acw->tree));
411 
412 	hbox = gtk_hbox_new(FALSE, 0);
413 	gtk_box_pack_start(GTK_BOX(hbox), acw->scroll, FALSE, TRUE, 0);
414 	acw->reflabel = gtk_label_new(NULL);
415 	gtk_label_set_line_wrap(GTK_LABEL(acw->reflabel), TRUE);
416 	gtk_misc_set_alignment(GTK_MISC(acw->reflabel), 0.1, 0.1);
417 	gtk_box_pack_start(GTK_BOX(hbox), acw->reflabel, TRUE, TRUE, 0);
418 	gtk_container_add(GTK_CONTAINER(acw->win), hbox);
419 	/*gtk_widget_set_size_request(acw->reflabel,150,-1); */
420 	gtk_widget_show_all(acw->scroll);
421 	gtk_widget_show(hbox);
422 	/*gtk_widget_set_size_request(GTK_WIDGET(acw->tree),100,200); */
423 	/*gtk_widget_set_size_request(acw->win, 150, 200); */
424 	/*g_signal_connect(G_OBJECT(acw->win),"key-release-event",G_CALLBACK(acwin_key_release_lcb),acw); */
425 
426 	return acw;
427 }
428 
429 /* returns TRUE if window is popped-up lower than the cursor,
430 returns FALSE if window is popped-up higher than the cursor (because cursor is low in the screen) */
431 static gboolean
acwin_position_at_cursor(BluefishTextView * btv)432 acwin_position_at_cursor(BluefishTextView * btv)
433 {
434 	GtkTextIter it;
435 	GdkRectangle rect;
436 	GdkScreen *screen;
437 	gint x, y, sh;
438 	GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(btv));
439 	screen = gtk_widget_get_screen(GTK_WIDGET(btv));
440 
441 	gtk_text_buffer_get_iter_at_mark(buffer, &it, gtk_text_buffer_get_insert(buffer));
442 	gtk_text_view_get_iter_location(GTK_TEXT_VIEW(btv), &it, &rect);
443 	gtk_text_view_buffer_to_window_coords(GTK_TEXT_VIEW(btv), GTK_TEXT_WINDOW_TEXT, rect.x, rect.y, &rect.x,
444 										  &rect.y);
445 	gdk_window_get_origin(gtk_text_view_get_window(GTK_TEXT_VIEW(btv), GTK_TEXT_WINDOW_TEXT), &x, &y);
446 
447 	sh = gdk_screen_get_height(screen);
448 	if (rect.y + y + ACWIN(btv->autocomp)->h > sh) {
449 		DBG_AUTOCOMP("acwin_position_at_cursor, popup above cursuor, rect.y+y=%d + rect.height(%d)-acw->h(%d)=%d, acw->h=%d, sh=%d\n"
450 				, rect.y + y,rect.height,ACWIN(btv->autocomp)->h
451 				, rect.y + y + rect.height - ACWIN(btv->autocomp)->h
452 				, ACWIN(btv->autocomp)->h, sh);
453 		gtk_window_move(GTK_WINDOW(ACWIN(btv->autocomp)->win), rect.x + x,
454 						rect.y + y + rect.height - ACWIN(btv->autocomp)->h);
455 		return FALSE;
456 	} else {
457 		DBG_AUTOCOMP("acwin_position_at_cursor, popup below cursuor, rect.y+y=%d, acw->h=%d, sh=%d\n", rect.y + y, ACWIN(btv->autocomp)->h, sh);
458 		gtk_window_move(GTK_WINDOW(ACWIN(btv->autocomp)->win), rect.x + x, rect.y + y);
459 		return TRUE;
460 	}
461 }
462 
463 static void
acwin_calculate_window_size(Tacwin * acw,GList * items,GList * items2,const gchar * closetag,gint * numitems)464 acwin_calculate_window_size(Tacwin * acw, GList * items, GList * items2, const gchar *closetag, gint *numitems)
465 {
466 	GList *tmplist;
467 	gboolean runtwo = FALSE;
468 	gchar *longest = NULL, *tmp;
469 	guint longestlen = 1;
470 	*numitems = 0;
471 	DBG_AUTOCOMP("acwin_calculate_window_size, items=%p, items2=%p, closetag=%s\n", items, items2, closetag);
472 	if (closetag) {
473 		longest = g_markup_escape_text(closetag, -1);
474 		longestlen = strlen(longest);
475 	}
476 	tmplist = g_list_first(items);
477 	while (!runtwo || tmplist) {
478 		guint len;
479 		if (!tmplist) {
480 			tmplist = g_list_first(items2);
481 			runtwo = TRUE;
482 			if (!tmplist)
483 				break;
484 		}
485 		g_assert(tmplist != NULL);
486 		g_assert(tmplist->data != NULL);
487 /*		DBG_AUTOCOMP("acwin_calculate_window_size, tmplist=%p", tmplist);
488 		DBG_AUTOCOMP(", tmplist->data=%p",tmplist->data);
489 		DBG_AUTOCOMP("=%s\n", (gchar *)tmplist->data);*/
490 		tmp = g_markup_escape_text(tmplist->data, -1);
491 		len = strlen(tmp);
492 		if (len > longestlen) {
493 			longest = tmp;
494 			longestlen = len;
495 		} else {
496 			g_free(tmp);
497 		}
498 		(*numitems)++;
499 		tmplist = g_list_next(tmplist);
500 	}
501 	if (longest) {
502 		gint len, rowh,min_h,nat_h;
503 		PangoLayout *panlay = gtk_widget_create_pango_layout(GTK_WIDGET(acw->tree), NULL);
504 		pango_layout_set_markup(panlay, longest, -1);
505 		pango_layout_get_pixel_size(panlay, &len, &rowh);
506 		DBG_AUTOCOMP("longest=%s which has len=%d and rowheight=%d\n", longest, len, rowh);
507 		/*  rowh+9 has been found by trial and error on a particular gtk theme and resolution, so
508 		there is quite a chance that this will not be optimal for other themes or for example
509 		on a high resolution display. I've tried to request this size but I cannot find it.
510 		I've now changed it to nat_h+5, hopefully nat_h is already a more dynamic value */
511 		/*gint spacing,height;
512 		GtkRequisition min_size, nat_size;
513 		gtk_cell_renderer_get_preferred_height(acw->cell,GTK_WIDGET(acw->tree),&min_h,&nat_h);
514 		spacing= gtk_tree_view_column_get_spacing(acw->column);
515 		gtk_tree_view_column_cell_get_size(acw->column,NULL,NULL,NULL,NULL,&height);
516 		gtk_widget_get_preferred_size(GTK_WIDGET(acw->tree),&min_size,&nat_size);
517 		g_print("row=%d,rowh+9=%d,min_h=%d,nat_h=%d,spacing=%d,height=%d,nat_size.h=%d\n",rowh,rowh+9,min_h,nat_h,spacing,height,min_size.height);*/
518 #if GTK_CHECK_VERSION(3,0,0)
519 		gtk_cell_renderer_get_preferred_height(acw->cell,GTK_WIDGET(acw->tree),&min_h,&nat_h);
520 #else
521 		nat_h=rowh+2;
522 #endif
523 		acw->h = MIN((*numitems) * (nat_h+5), 350); /*MIN(MAX((*numitems + 1) * rowh + 8, 150), 350);*/
524 		acw->w = acw->listwidth = MIN(len + 20, 350);
525 		DBG_AUTOCOMP("acwin_calculate_window_size, numitems=%d, rowh=%d, new height=%d, new width=%d\n", *numitems, rowh, acw->h,
526 				acw->listwidth);
527 		gtk_widget_set_size_request(GTK_WIDGET(acw->scroll), acw->listwidth, acw->h);	/* ac_window */
528 		g_free(longest);
529 		g_object_unref(G_OBJECT(panlay));
530 	}
531 }
532 
533 static int
ac_sort_func(const gchar * a,const gchar * b)534 ac_sort_func(const gchar *a, const gchar *b)
535 {
536 	if (a && b && a[0] == '<' && b[0] == '<' && a[1] != b[1]) {
537 		/* make sure that </ is sorted below <[a-z] */
538 		if (a[1] == '/') return 1;
539 		if (b[1] == '/') return -1;
540 		return a[1]-b[1];
541 	}
542 	return g_strcmp0(a,b);
543 }
544 
545 /* fills the tree */
546 static void
acwin_fill_tree(Tacwin * acw,GList * items,GList * items2,gchar * closetag,gboolean reverse,gint numitems)547 acwin_fill_tree(Tacwin * acw, GList * items, GList * items2, gchar * closetag, gboolean reverse, gint numitems)
548 {
549 	GList *tmplist, *list = NULL;
550 	gint everynth=1, i=0;
551 
552 	/* to avoid that we show 10000 items in the autocomplete list, we insert every N items where N % everynth == 0 */
553 	if (numitems > 512) {
554 		everynth = 1.0 * numitems / 512.0;
555 		DBG_AUTOCOMP("numitems=%d, everynth=%d\n", numitems, everynth);
556 	}
557 	if (items)
558 		list = g_list_copy(items);
559 	if (items2)
560 		list = g_list_concat(g_list_copy(items2), list);
561 
562 	list = g_list_sort(list, (GCompareFunc) ac_sort_func);
563 	if (closetag) {
564 		GList *tlist2;
565 		tlist2 = find_in_stringlist(list, closetag);
566 		if (tlist2) {
567 			list = g_list_remove_link(list, tlist2);
568 			list = g_list_concat(tlist2, list);
569 		} else {
570 			list = g_list_prepend(list, closetag);
571 		}
572 	}
573 	if (reverse) {
574 		list = g_list_reverse(list);
575 		DBG_AUTOCOMP("reverse list!\n");
576 	}
577 	tmplist = g_list_first(list);
578 	while (tmplist) {
579 		if (i % everynth == 0) {
580 			GtkTreeIter it;
581 			gchar *tmp;
582 			gtk_list_store_append(acw->store, &it);
583 			tmp = g_markup_escape_text(tmplist->data, -1);
584 			gtk_list_store_set(acw->store, &it, 0, tmp, 1, tmplist->data, -1);
585 			/*DBG_AUTOCOMP("acwin_fill_tree, add item %s\n",tmp);*/
586 			g_free(tmp);
587 		}
588 		i++;
589 		tmplist = g_list_next(tmplist);
590 	}
591 	g_list_free(list);
592 }
593 
594 /* static void print_ac_items(GCompletion *gc) {
595 	GList *tmplist = g_list_first(gc->items);
596 	DBG_AUTOCOMP("autocompletion has %d items:",g_list_length(g_list_first(gc->items)));
597 	while (tmplist) {
598 		DBG_AUTOCOMP(" %s",(char *)tmplist->data);
599 		tmplist = g_list_next(tmplist);
600 	}
601 	DBG_AUTOCOMP("\n");
602 } */
603 
604 void
autocomp_stop(BluefishTextView * btv)605 autocomp_stop(BluefishTextView * btv)
606 {
607 	acwin_cleanup(btv);
608 }
609 
610 static GList *
process_conditional_items(BluefishTextView * btv,Tfound * found,gint contextnum,GList * items)611 process_conditional_items(BluefishTextView * btv, Tfound *found, gint contextnum, GList *items)
612 {
613 	BluefishTextView *master = BLUEFISH_TEXT_VIEW(btv->master);
614 	GList *retval=NULL, *tmplist=items;
615 
616 	while (tmplist) {
617 		gboolean valid=TRUE;
618 		guint pattern_id;
619 		const gchar *string = tmplist->data;
620 		GHashTable *hasht = g_array_index(master->bflang->st->contexts, Tcontext,contextnum).patternhash;
621 		DBG_AUTOCOMP("process_conditional_items, looking up %s in hashtable %p\n",string, hasht);
622 		pattern_id = GPOINTER_TO_INT(g_hash_table_lookup(hasht, string));
623 		DBG_AUTOCOMP("process_conditional_items, got pattern_id=%d\n", pattern_id);
624 		if (pattern_id && found) {
625 				/* found may be NULL, for examplein an empty document, in that case anything is valid */
626 			GSList *tmpslist = g_array_index(master->bflang->st->matches, Tpattern, pattern_id).autocomp_items;
627 			/* a pattern MAY have multiple autocomplete items. This code is not efficient if in the future some
628 			   patterns would have many autocomplete items. I don't expect this, so I leave this as it is right now  */
629 			while (tmpslist) {
630 				Tpattern_autocomplete *pac = tmpslist->data;
631 				if (g_strcmp0(string, pac->autocomplete_string) == 0 && pac->condition!=0) {
632 					Tpattern_condition *pcond = &g_array_index(master->bflang->st->conditions, Tpattern_condition, pac->condition);
633 
634 					valid = test_condition(found->fcontext, found->fblock, pcond);
635 					if (!valid) {
636 						DBG_AUTOCOMP("process_conditional_items, item %s is NOT VALID for autocompletion\n",string);
637 					}
638 				}
639 				tmpslist = g_slist_next(tmpslist);
640 			}
641 		}
642 		if (valid) {
643 			retval = g_list_prepend(retval, string);
644 		}
645 
646 		tmplist = tmplist->next;
647 	}
648 	return retval;
649 }
650 
651 void
autocomp_run(BluefishTextView * btv,gboolean user_requested)652 autocomp_run(BluefishTextView * btv, gboolean user_requested)
653 {
654 	GtkTextIter cursorpos, iter;
655 	BluefishTextView *master = BLUEFISH_TEXT_VIEW(btv->master);
656 	gint contextnum;
657 	gunichar uc;
658 
659 	Tfound *found=NULL;
660 	Tfoundblock *fblock = NULL;	/* needed for the special case to close generix xml tags
661 											based on the top of the blockstack, or to match conditional
662 											autocompletion strings */
663 
664 	if (G_UNLIKELY(!master->bflang || !master->bflang->st))
665 		return;
666 
667 	gtk_text_buffer_get_iter_at_mark(btv->buffer, &cursorpos, gtk_text_buffer_get_insert(btv->buffer));
668 
669 	iter = cursorpos;
670 	gtk_text_iter_set_line_offset(&iter, 0);
671 
672 
673 	scan_for_autocomp_prefix(master, &iter, &cursorpos, &contextnum);
674 	DBG_AUTOCOMP("autocomp_run, got possible match start at %d in context %d, cursor is at %d\n",
675 				 gtk_text_iter_get_offset(&iter), contextnum, gtk_text_iter_get_offset(&cursorpos));
676 	/* see if character at cursor is end or symbol */
677 	uc = gtk_text_iter_get_char(&cursorpos);
678 	if (G_UNLIKELY(uc > NUMSCANCHARS))
679 		return;
680 
681 	/*identstate = g_array_index(master->bflang->st->contexts, Tcontext, contextnum).identstate;*/
682 	if (!character_is_symbol(master->bflang->st,contextnum,uc)) {
683 		/* current character is not a symbol! */
684 		DBG_AUTOCOMP("autocomp_run, character at cursor %d '%c' is not a symbol, return\n", uc, (char) uc);
685 		acwin_cleanup(btv);
686 		return;
687 	}
688 
689 	/* see if we have enough characters */
690 	if (!user_requested && gtk_text_iter_get_offset(&cursorpos) - gtk_text_iter_get_offset(&iter) < main_v->props.autocomp_min_prefix_len) {
691 		DBG_AUTOCOMP("autocomp_run, prefix len %d < autocomp_min_prefix_len (%d), abort!\n"
692 					, gtk_text_iter_get_offset(&cursorpos) - gtk_text_iter_get_offset(&iter)
693 					, main_v->props.autocomp_min_prefix_len);
694 		acwin_cleanup(btv);
695 		return;
696 	}
697 
698 	if (g_array_index(master->bflang->st->contexts, Tcontext, contextnum).has_tagclose_from_blockstack) {
699 		found = get_foundcache_at_offset(btv, gtk_text_iter_get_offset(&cursorpos), NULL);
700 		if (found) {
701 			fblock =
702 				found->numblockchange < 0 ? pop_blocks(found->numblockchange, found->fblock) : found->fblock;
703 			if (fblock && fblock->start2_o != BF_OFFSET_UNDEFINED) {
704 				DBG_AUTOCOMP("abort offering closing tag: block has an end already\n");
705 				fblock = NULL;
706 			}
707 /*		if (g_array_index(btv->bflang->st->matches, Tpattern, fblock->patternum).tagclose_from_blockstack) {
708 				gchar *start;
709 				gtk_text_buffer_get_iter_at_mark(buffer, &it1, fblock->start1);
710 				gtk_text_buffer_get_iter_at_mark(buffer, &it2, fblock->end1);
711 				gtk_text_iter_forward_char(&it1);
712 				start = gtk_text_buffer_get_text(buffer,&it1,&it2,TRUE);
713 				g_print("close tag %s\n",start);
714 				g_free(start);
715 			}*/
716 		}
717 	}
718 	if ((user_requested || !gtk_text_iter_equal(&iter, &cursorpos))
719 		&& (g_array_index(master->bflang->st->contexts, Tcontext, contextnum).ac != NULL
720 			|| (fblock
721 				&& g_array_index(master->bflang->st->matches, Tpattern,
722 								 fblock->patternum).tagclose_from_blockstack)
723 		)
724 		) {
725 		/* we have a prefix or it is user requested, and we have a context with autocompletion or we have blockstack-tag-auto-closing */
726 		gchar *newprefix = NULL, *prefix, *closetag = NULL;
727 		GList *items = NULL, *items2 = NULL;
728 		gboolean free_items=FALSE;
729 		/*print_ac_items(g_array_index(btv->bflang->st->contexts,Tcontext, contextnum).ac); */
730 
731 		prefix = gtk_text_buffer_get_text(btv->buffer, &iter, &cursorpos, TRUE);
732 
733 		if (fblock) {
734 			GString *tmpstr;
735 			gint plen;
736 			GtkTextIter it1;
737 			gtk_text_buffer_get_iter_at_offset(btv->buffer, &it1, fblock->start1_o);
738 			tmpstr = g_string_new("</");
739 			while(gtk_text_iter_forward_char(&it1)) {
740 				gunichar uc = gtk_text_iter_get_char(&it1);
741 				if (!g_unichar_isalnum(uc) && uc != '_') {
742 					break;
743 				}
744 				g_string_append_c(tmpstr, uc);
745 			}
746 			g_string_append_c(tmpstr, '>');
747 			closetag = g_string_free(tmpstr, FALSE);
748 			DBG_AUTOCOMP("closetag=%s, prefix=%s\n", closetag, prefix);
749 			plen = strlen(prefix);
750 			if (plen == strlen(closetag) || strncmp(closetag, prefix, plen) != 0) {
751 				g_free(closetag);
752 				closetag = NULL;
753 			}
754 		}
755 		if (g_array_index(master->bflang->st->contexts, Tcontext, contextnum).ac) {
756 			items =
757 				g_completion_complete(g_array_index(master->bflang->st->contexts, Tcontext, contextnum).ac,
758 									  prefix, &newprefix);
759 			DBG_AUTOCOMP("got %d autocompletion items for prefix %s in context %d, newprefix=%s\n",
760 						 g_list_length(items), prefix, contextnum, newprefix);
761 			if (G_UNLIKELY(g_array_index(master->bflang->st->contexts, Tcontext, contextnum).autocomplete_has_conditions)) {
762 				if (found==NULL) {
763 					found = get_foundcache_at_offset(btv, gtk_text_iter_get_offset(&cursorpos), NULL);
764 				}
765 				items = process_conditional_items(btv, found, contextnum, items);
766 				free_items=TRUE;
767 			}
768 			{
769 				GCompletion *compl = identifier_ac_get_completion(master, contextnum, FALSE);
770 				DBG_IDENTIFIER("got completion %p for context %d\n", compl, contextnum);
771 				if (compl) {
772 					gchar *newprefix2 = NULL;
773 					items2 = g_completion_complete(compl, prefix, &newprefix2);
774 					DBG_IDENTIFIER("got %d identifier_items for prefix %s, newprefix=%s\n", g_list_length(items2), prefix, newprefix2);
775 					if (!newprefix)
776 						newprefix = newprefix2;
777 					else
778 						g_free(newprefix2);
779 				}
780 			}
781 		}
782 		if (closetag || items2
783 			|| (items != NULL && (items->next != NULL || strcmp(items->data, prefix) != 0))) {
784 			/* do not popup if there are 0 items, and also not if there is 1 item which equals the prefix */
785 			GtkTreeSelection *selection;
786 			GtkTreeIter it;
787 			gboolean below;
788 			gint numitems=0;
789 			/* create the GUI */
790 			if (!btv->autocomp) {
791 				btv->autocomp = acwin_create(btv);
792 			} else {
793 				ACWIN(btv->autocomp)->in_fill=TRUE;
794 				g_free(ACWIN(btv->autocomp)->prefix);
795 				g_free(ACWIN(btv->autocomp)->newprefix);
796 				ACWIN(btv->autocomp)->prefix = NULL;
797 				ACWIN(btv->autocomp)->newprefix = NULL;
798 				gtk_list_store_clear(ACWIN(btv->autocomp)->store);
799 			}
800 			ACWIN(btv->autocomp)->contextnum = contextnum;
801 			ACWIN(btv->autocomp)->prefix = g_strdup(prefix);
802 			if (newprefix) {
803 				ACWIN(btv->autocomp)->newprefix = g_strdup(newprefix);
804 			}
805 			acwin_calculate_window_size(ACWIN(btv->autocomp), items, items2, closetag, &numitems);
806 			below = acwin_position_at_cursor(btv);
807 			acwin_fill_tree(ACWIN(btv->autocomp), items, items2, closetag, !below, numitems);
808 			gtk_widget_show(ACWIN(btv->autocomp)->win);
809 			selection = gtk_tree_view_get_selection(ACWIN(btv->autocomp)->tree);
810 			if (below) {
811 				DBG_AUTOCOMP("autocomp_run, popup-below, get first iter for selection\n");
812 				gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ACWIN(btv->autocomp)->store), &it);
813 			} else {
814 				GtkTreePath *path;
815 				DBG_AUTOCOMP("autocomp_run, popup-above, select last iter and scroll max down\n");
816 				gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(ACWIN(btv->autocomp)->store), &it, NULL,
817 											  gtk_tree_model_iter_n_children(GTK_TREE_MODEL
818 																			 (ACWIN(btv->autocomp)->store),
819 																			 NULL) - 1);
820 				path = gtk_tree_model_get_path(GTK_TREE_MODEL(ACWIN(btv->autocomp)->store), &it);
821 				gtk_tree_view_scroll_to_cell(ACWIN(btv->autocomp)->tree, path, NULL, FALSE, 1.0, 1.0);
822 				gtk_tree_path_free(path);
823 			}
824 			/* this forces that selection changed will be called two lines below*/
825 			gtk_tree_selection_unselect_all(selection);
826 			ACWIN(btv->autocomp)->in_fill=FALSE;
827 			DBG_AUTOCOMP("call select_iter on autocomp window\n");
828 			gtk_tree_selection_select_iter(selection, &it);
829 			g_free(closetag);
830 		} else {
831 			acwin_cleanup(btv);
832 		}
833 		if (free_items) {
834 			g_list_free(items);
835 		}
836 		g_free(newprefix);
837 		g_free(prefix);
838 	} else {
839 		DBG_AUTOCOMP("no autocompletion data for context %d (ac=%p), or no prefix\n", contextnum,
840 					 g_array_index(master->bflang->st->contexts, Tcontext, contextnum).ac);
841 		acwin_cleanup(btv);
842 	}
843 }
844