1 /*
2  *      navqueue.c - this file is part of Geany, a fast and lightweight IDE
3  *
4  *      Copyright 2007 The Geany contributors
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 2 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 along
17  *      with this program; if not, write to the Free Software Foundation, Inc.,
18  *      51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19  */
20 
21 /*
22  * Simple code navigation
23  */
24 
25 #ifdef HAVE_CONFIG_H
26 # include "config.h"
27 #endif
28 
29 #include "navqueue.h"
30 
31 #include "document.h"
32 #include "geanyobject.h"
33 #include "sciwrappers.h"
34 #include "toolbar.h"
35 #include "utils.h"
36 
37 #include "gtkcompat.h"
38 
39 
40 /* for the navigation history queue */
41 typedef struct
42 {
43 	const gchar *file;	/* This is the document's filename, in UTF-8 */
44 	gint pos;
45 } filepos;
46 
47 static GQueue *navigation_queue;
48 static guint nav_queue_pos;
49 
50 static GtkAction *navigation_buttons[2];
51 
52 
53 
navqueue_init(void)54 void navqueue_init(void)
55 {
56 	navigation_queue = g_queue_new();
57 	nav_queue_pos = 0;
58 
59 	navigation_buttons[0] = toolbar_get_action_by_name("NavBack");
60 	navigation_buttons[1] = toolbar_get_action_by_name("NavFor");
61 
62 	gtk_action_set_sensitive(navigation_buttons[0], FALSE);
63 	gtk_action_set_sensitive(navigation_buttons[1], FALSE);
64 }
65 
66 
navqueue_free(void)67 void navqueue_free(void)
68 {
69 	while (! g_queue_is_empty(navigation_queue))
70 	{
71 		g_free(g_queue_pop_tail(navigation_queue));
72 	}
73 	g_queue_free(navigation_queue);
74 }
75 
76 
adjust_buttons(void)77 static void adjust_buttons(void)
78 {
79 	if (g_queue_get_length(navigation_queue) < 2)
80 	{
81 		gtk_action_set_sensitive(navigation_buttons[0], FALSE);
82 		gtk_action_set_sensitive(navigation_buttons[1], FALSE);
83 		return;
84 	}
85 	if (nav_queue_pos == 0)
86 	{
87 		gtk_action_set_sensitive(navigation_buttons[0], TRUE);
88 		gtk_action_set_sensitive(navigation_buttons[1], FALSE);
89 		return;
90 	}
91 	/* forward should be sensitive since where not at the start */
92 	gtk_action_set_sensitive(navigation_buttons[1], TRUE);
93 
94 	/* back should be sensitive if there's a place to go back to */
95 	(nav_queue_pos < g_queue_get_length(navigation_queue) - 1) ?
96 		gtk_action_set_sensitive(navigation_buttons[0], TRUE) :
97 			gtk_action_set_sensitive(navigation_buttons[0], FALSE);
98 }
99 
100 
101 static gboolean
queue_pos_matches(guint queue_pos,const gchar * fname,gint pos)102 queue_pos_matches(guint queue_pos, const gchar *fname, gint pos)
103 {
104 	if (queue_pos < g_queue_get_length(navigation_queue))
105 	{
106 		filepos *fpos = g_queue_peek_nth(navigation_queue, queue_pos);
107 
108 		return (utils_str_equal(fpos->file, fname) && fpos->pos == pos);
109 	}
110 	return FALSE;
111 }
112 
113 
add_new_position(const gchar * utf8_filename,gint pos)114 static void add_new_position(const gchar *utf8_filename, gint pos)
115 {
116 	filepos *npos;
117 	guint i;
118 
119 	if (queue_pos_matches(nav_queue_pos, utf8_filename, pos))
120 		return;	/* prevent duplicates */
121 
122 	npos = g_new0(filepos, 1);
123 	npos->file = utf8_filename;
124 	npos->pos = pos;
125 
126 	/* if we've jumped to a new position from inside the queue rather than going forward */
127 	if (nav_queue_pos > 0)
128 	{
129 		for (i = 0; i < nav_queue_pos; i++)
130 		{
131 			g_free(g_queue_pop_head(navigation_queue));
132 		}
133 		nav_queue_pos = 0;
134 	}
135 	g_queue_push_head(navigation_queue, npos);
136 	adjust_buttons();
137 }
138 
139 
140 /**
141  *  Adds old file position and new file position to the navqueue, then goes to the new position.
142  *
143  *  @param old_doc The document of the previous position, if set as invalid (@c NULL) then no old
144  *         position is set
145  *  @param new_doc The document of the new position, must be valid.
146  *  @param line the line number of the new position. It is counted with 1 as the first line, not 0.
147  *
148  *  @return @c TRUE if the cursor has changed the position to @a line or @c FALSE otherwise.
149  **/
150 GEANY_API_SYMBOL
navqueue_goto_line(GeanyDocument * old_doc,GeanyDocument * new_doc,gint line)151 gboolean navqueue_goto_line(GeanyDocument *old_doc, GeanyDocument *new_doc, gint line)
152 {
153 	gint pos;
154 
155 	g_return_val_if_fail(old_doc == NULL || old_doc->is_valid, FALSE);
156 	g_return_val_if_fail(DOC_VALID(new_doc), FALSE);
157 	g_return_val_if_fail(line >= 1, FALSE);
158 
159 	pos = sci_get_position_from_line(new_doc->editor->sci, line - 1);
160 
161 	/* first add old file position */
162 	if (old_doc != NULL && old_doc->file_name)
163 	{
164 		gint cur_pos = sci_get_current_position(old_doc->editor->sci);
165 
166 		add_new_position(old_doc->file_name, cur_pos);
167 	}
168 
169 	/* now add new file position */
170 	if (new_doc->file_name)
171 	{
172 		add_new_position(new_doc->file_name, pos);
173 	}
174 
175 	return editor_goto_pos(new_doc->editor, pos, TRUE);
176 }
177 
178 
goto_file_pos(const gchar * file,gint pos)179 static gboolean goto_file_pos(const gchar *file, gint pos)
180 {
181 	GeanyDocument *doc = document_find_by_filename(file);
182 
183 	if (doc == NULL)
184 		return FALSE;
185 
186 	return editor_goto_pos(doc->editor, pos, TRUE);
187 }
188 
189 
navqueue_go_back(void)190 void navqueue_go_back(void)
191 {
192 	filepos *fprev;
193 	GeanyDocument *doc = document_get_current();
194 
195 	/* If the navqueue is currently at some position A, but the actual cursor is at some other
196 	 * place B, we should add B to the navqueue, so that (1) we go back to A, not to the next
197 	 * item in the queue; and (2) we can later restore B by going forward.
198 	 * (If A = B, add_new_position will ignore it.) */
199 	if (doc)
200 	{
201 		if (doc->file_name)
202 			add_new_position(doc->file_name, sci_get_current_position(doc->editor->sci));
203 	}
204 	else
205 		/* see also https://github.com/geany/geany/pull/1537 */
206 		g_warning("Attempted navigation when nothing is open");
207 
208 	/* return if theres no place to go back to */
209 	if (g_queue_is_empty(navigation_queue) ||
210 		nav_queue_pos >= g_queue_get_length(navigation_queue) - 1)
211 		return;
212 
213 	/* jump back */
214 	fprev = g_queue_peek_nth(navigation_queue, nav_queue_pos + 1);
215 	if (goto_file_pos(fprev->file, fprev->pos))
216 	{
217 		nav_queue_pos++;
218 	}
219 	else
220 	{
221 		/** TODO: add option to re open the file */
222 		g_free(g_queue_pop_nth(navigation_queue, nav_queue_pos + 1));
223 	}
224 	adjust_buttons();
225 }
226 
227 
navqueue_go_forward(void)228 void navqueue_go_forward(void)
229 {
230 	filepos *fnext;
231 
232 	if (nav_queue_pos < 1 ||
233 		nav_queue_pos >= g_queue_get_length(navigation_queue))
234 		return;
235 
236 	/* jump forward */
237 	fnext = g_queue_peek_nth(navigation_queue, nav_queue_pos - 1);
238 	if (goto_file_pos(fnext->file, fnext->pos))
239 	{
240 		nav_queue_pos--;
241 	}
242 	else
243 	{
244 		/** TODO: add option to re open the file */
245 		g_free(g_queue_pop_nth(navigation_queue, nav_queue_pos - 1));
246 	}
247 
248 	adjust_buttons();
249 }
250 
251 
find_by_filename(gconstpointer a,gconstpointer b)252 static gint find_by_filename(gconstpointer a, gconstpointer b)
253 {
254 	if (utils_str_equal(((const filepos*)a)->file, (const gchar*) b))
255 		return 0;
256 	else
257 		return 1;
258 }
259 
260 
261 /* Remove all elements with the given filename */
navqueue_remove_file(const gchar * filename)262 void navqueue_remove_file(const gchar *filename)
263 {
264 	GList *match;
265 
266 	if (filename == NULL)
267 		return;
268 
269 	while ((match = g_queue_find_custom(navigation_queue, filename, find_by_filename)))
270 	{
271 		g_free(match->data);
272 		g_queue_delete_link(navigation_queue, match);
273 	}
274 
275 	adjust_buttons();
276 }
277