1 /*
2  *      lineoperations.c - Line operations, remove duplicate lines, empty lines,
3  *                         lines with only whitespace, sort lines.
4  *
5  *      Copyright 2015 Sylvan Mostert <smostert.dev@gmail.com>
6  *
7  *      This program is free software; you can redistribute it and/or modify
8  *      it under the terms of the GNU General Public License as published by
9  *      the Free Software Foundation; either version 2 of the License, or
10  *      (at your option) any later version.
11  *
12  *      This program is distributed in the hope that it will be useful,
13  *      but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *      GNU General Public License for more details.
16  *
17  *      You should have received a copy of the GNU General Public License along
18  *      with this program; if not, write to the Free Software Foundation, Inc.,
19  *      51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 */
21 
22 
23 #ifdef HAVE_CONFIG_H
24     #include "config.h" /* for the gettext domain */
25 #endif
26 
27 #include <geanyplugin.h>
28 #include "Scintilla.h"
29 #include "lo_fns.h"
30 #include "lo_prefs.h"
31 
32 
33 static GtkWidget *main_menu_item = NULL;
34 
35 
36 /* represents a selection of lines that will have Operation applied to */
37 struct lo_lines
38 {
39 	gboolean is_selection;
40 	gint     start_line;
41 	gint     end_line;
42 };
43 
44 
45 /* represents a menu item and key binding */
46 struct lo_menu_item
47 {
48 	const gchar *label;
49 	const gchar *kb_section_name;
50 	GCallback cb_activate;
51 	gpointer cb_data;
52 };
53 
54 
55 typedef void (*CB_USER_FUNCTION)(GtkMenuItem *menuitem, gpointer gdata);
56 
57 
58 /* selects lines in document (based on lo_lines struct parameter) */
59 static void
select_lines(GeanyEditor * editor,struct lo_lines * sel)60 select_lines(GeanyEditor *editor, struct lo_lines *sel)
61 {
62 	/* set the selection to beginning of first line */
63 	sci_set_selection_start(editor->sci,
64 							sci_get_position_from_line(editor->sci, sel->start_line));
65 
66 	/* set the selection to end of last line */
67 	sci_set_selection_end(editor->sci,
68 						  sci_get_line_end_position(editor->sci, sel->end_line) +
69 							  editor_get_eol_char_len(editor));
70 }
71 
72 
73 /* get lo_lines struct 'sel' from document */
74 static void
get_current_sel_lines(ScintillaObject * sci,struct lo_lines * sel)75 get_current_sel_lines(ScintillaObject *sci, struct lo_lines *sel)
76 {
77 	gint start_posn     = 0;        /* position of selection start */
78 	gint end_posn       = 0;        /* position of selection end   */
79 
80 	/* check for selection */
81 	if (sci_has_selection(sci))
82 	{
83 		/* get the start and end *positions* */
84 		start_posn = sci_get_selection_start(sci);
85 		end_posn   = sci_get_selection_end  (sci);
86 
87 		/* get the *line number* of those positions */
88 		sel->start_line = scintilla_send_message(sci,
89 									SCI_LINEFROMPOSITION,
90 									start_posn, 0);
91 
92 		sel->end_line   = scintilla_send_message(sci,
93 									SCI_LINEFROMPOSITION,
94 									end_posn, 0);
95 
96 		sel->is_selection = TRUE;
97 	}
98 	else
99 	{
100 		/* if there is no selection, start at first line */
101 		sel->start_line = 0;
102 		/* and end at last one */
103 		sel->end_line   = (sci_get_line_count(sci) - 1);
104 
105 		sel->is_selection = FALSE;
106 	}
107 }
108 
109 
110 /* altered from geany/src/editor.c, ensure new line at file end */
111 static void
ensure_final_newline(GeanyEditor * editor,gint * num_lines,struct lo_lines * sel)112 ensure_final_newline(GeanyEditor *editor, gint *num_lines, struct lo_lines *sel)
113 {
114 	gint end_document   = sci_get_position_from_line(editor->sci, (*num_lines));
115 	gboolean append_newline = end_document >
116 					sci_get_position_from_line(editor->sci, ((*num_lines) - 1));
117 
118 	if (append_newline)
119 	{
120 		const gchar *eol = editor_get_eol_char(editor);
121 		sci_insert_text(editor->sci, end_document, eol);
122 
123 		/* re-adjust the selection */
124 		(*num_lines)++;
125 		sel->end_line++;
126 	}
127 }
128 
129 
130 /* set statusbar with message and select altered lines */
131 static void
user_indicate(GeanyEditor * editor,gint lines_affected,struct lo_lines * sel)132 user_indicate(GeanyEditor *editor, gint lines_affected, struct lo_lines *sel)
133 {
134 	if (lines_affected < 0)
135 	{
136 		ui_set_statusbar(FALSE, _("Operation successful! %d lines removed."),
137 					-lines_affected);
138 
139 		/* select lines to indicate to user what lines were altered */
140 		sel->end_line += lines_affected;
141 
142 		if (sel->is_selection)
143 			select_lines(editor, sel);
144 	}
145 	else if (lines_affected == 0)
146 	{
147 		ui_set_statusbar(FALSE, _("Operation successful! No lines removed."));
148 
149 		/* select lines to indicate to user what lines were altered */
150 		if (sel->is_selection)
151 			select_lines(editor, sel);
152 	}
153 	else
154 	{
155 		ui_set_statusbar(FALSE, _("Operation successful! %d lines affected."),
156 					lines_affected);
157 
158 		/* select lines to indicate to user what lines were altered */
159 		if (sel->is_selection)
160 			select_lines(editor, sel);
161 	}
162 }
163 
164 
165 /*
166  * Menu action for functions with indirect scintilla manipulation
167  * e.g. functions requiring **lines array, num_lines, *new_file
168  *
169  * Use 'action_sci_manip_item()' if possible, since direction
170  * manipulation of Scintilla doc is faster/better.
171  * Use this if the line operation cannot be easily done with
172  * scintilla functions.
173 */
174 static void
action_indir_manip_item(GtkMenuItem * menuitem,gpointer gdata)175 action_indir_manip_item(GtkMenuItem *menuitem, gpointer gdata)
176 {
177 	/* function pointer to function to be used */
178 	gint (*func)(gchar **lines, gint num_lines, gchar *new_file) = gdata;
179 	GeanyDocument *doc    = document_get_current();
180 	g_return_if_fail(doc != NULL);
181 
182 	struct lo_lines sel;
183 	gint   num_chars      = 0;
184 	gint   i              = 0;
185 	gint   lines_affected = 0;
186 
187 	get_current_sel_lines(doc->editor->sci, &sel);
188 	gint num_lines = (sel.end_line - sel.start_line) + 1;
189 
190 	/* if last line within selection ensure that the file ends with newline */
191 	if ((sel.end_line + 1) == sci_get_line_count(doc->editor->sci))
192 		ensure_final_newline(doc->editor, &num_lines, &sel);
193 
194 	/* get num_chars and **lines */
195 	gchar **lines = g_malloc(sizeof(gchar *) * num_lines);
196 	for (i = 0; i < num_lines; i++)
197 	{
198 		num_chars += (sci_get_line_length(doc->editor->sci,
199 										(i + sel.start_line)));
200 
201 		lines[i]   = sci_get_line(doc->editor->sci,
202 										(i + sel.start_line));
203 	}
204 
205 	gchar *new_file  = g_malloc(sizeof(gchar) * (num_chars + 1));
206 	new_file[0]      = '\0';
207 
208 	/* select lines that will be replaced with array */
209 	select_lines(doc->editor, &sel);
210 
211 	sci_start_undo_action(doc->editor->sci);
212 
213 	lines_affected = func(lines, num_lines, new_file);
214 
215 
216 	/* set new document */
217 	sci_replace_sel(doc->editor->sci, new_file);
218 
219 	/* select affected lines and set statusbar message */
220 	user_indicate(doc->editor, lines_affected, &sel);
221 
222 	sci_end_undo_action(doc->editor->sci);
223 
224 	/* free used memory */
225 	for (i = 0; i < num_lines; i++)
226 		g_free(lines[i]);
227 	g_free(lines);
228 	g_free(new_file);
229 }
230 
231 
232 /*
233  * Menu action for functions with direct scintilla manipulation
234  * e.g. no need for **lines array, *new_file...
235  *
236  * Use this if the line operation can be directly done with
237  * scintilla functions.
238 */
239 static void
action_sci_manip_item(GtkMenuItem * menuitem,gpointer gdata)240 action_sci_manip_item(GtkMenuItem *menuitem, gpointer gdata)
241 {
242 	/* function pointer to gdata -- function to be used */
243 	gint (*func)(ScintillaObject *, gint, gint) = gdata;
244 	GeanyDocument *doc = document_get_current();
245 	g_return_if_fail(doc != NULL);
246 
247 	struct lo_lines sel;
248 	get_current_sel_lines(doc->editor->sci, &sel);
249 	gint lines_affected = 0;
250 
251 	sci_start_undo_action(doc->editor->sci);
252 
253 	lines_affected = func(doc->editor->sci, sel.start_line, sel.end_line);
254 
255 
256 	/* put message in ui_statusbar, and highlight lines that were affected */
257 	user_indicate(doc->editor, lines_affected, &sel);
258 
259 	sci_end_undo_action(doc->editor->sci);
260 }
261 
262 
263 /* List of menu items, also used for keybindings. */
264 static struct lo_menu_item menu_items[] = {
265 	{ N_("Remove Duplicate Lines, _Sorted"), "remove_duplicate_lines_s",
266 	  G_CALLBACK(action_indir_manip_item), (gpointer) rmdupst },
267 	{ N_("Remove Duplicate Lines, _Ordered"), "remove_duplicate_lines_o",
268 	  G_CALLBACK(action_indir_manip_item), (gpointer) rmdupln },
269 	{ N_("Remove _Unique Lines"), "remove_unique_lines",
270 	  G_CALLBACK(action_indir_manip_item), (gpointer) rmunqln },
271 	{ N_("Keep _Unique Lines"), "keep_unique_lines",
272 	  G_CALLBACK(action_indir_manip_item), (gpointer) kpunqln },
273 	{ NULL },
274 	{ N_("Remove _Empty Lines"), "remove_empty_lines",
275 	  G_CALLBACK(action_sci_manip_item), (gpointer) rmemtyln },
276 	{ N_("Remove _Whitespace Lines"), "remove_whitespace_lines",
277 	  G_CALLBACK(action_sci_manip_item), (gpointer) rmwhspln },
278 	{ NULL },
279 	{ N_("Remove Every _Nth Line"), "remove_every_nth_line",
280 	  G_CALLBACK(action_sci_manip_item), (gpointer) rmnthln },
281 	{ NULL },
282 	{ N_("Sort Lines _Ascending"), "sort_lines_ascending",
283 	  G_CALLBACK(action_indir_manip_item), (gpointer) sortlnsasc },
284 	{ N_("Sort Lines _Descending"), "sort_lines_descending",
285 	  G_CALLBACK(action_indir_manip_item), (gpointer) sortlndesc }
286 };
287 
288 
289 /* Keybinding callback */
lo_keybinding_callback(guint key_id)290 static void lo_keybinding_callback(guint key_id)
291 {
292 	CB_USER_FUNCTION cb_activate;
293 	g_return_if_fail(key_id < G_N_ELEMENTS(menu_items));
294 	cb_activate = (CB_USER_FUNCTION)menu_items[key_id].cb_activate;
295 	cb_activate(NULL, menu_items[key_id].cb_data);
296 }
297 
298 
299 /* Initialization */
300 static gboolean
lo_init(GeanyPlugin * plugin,G_GNUC_UNUSED gpointer gdata)301 lo_init(GeanyPlugin *plugin, G_GNUC_UNUSED gpointer gdata)
302 {
303 	GeanyData *geany_data = plugin->geany_data;
304 	GeanyKeyGroup *key_group;
305 	GtkWidget *submenu;
306 	guint i;
307 
308 	lo_init_prefs(plugin);
309 
310 	main_menu_item = gtk_menu_item_new_with_mnemonic(_("_Line Operations"));
311 	gtk_widget_show(main_menu_item);
312 
313 	submenu = gtk_menu_new();
314 	gtk_widget_show(submenu);
315 
316 	for (i = 0; i < G_N_ELEMENTS(menu_items); i++)
317 	{
318 		GtkWidget *item;
319 
320 		if (! menu_items[i].label) /* separator */
321 			item = gtk_separator_menu_item_new();
322 		else
323 		{
324 			item = gtk_menu_item_new_with_mnemonic(_(menu_items[i].label));
325 			g_signal_connect(item,
326 							"activate",
327 							menu_items[i].cb_activate,
328 							menu_items[i].cb_data);
329 			ui_add_document_sensitive(item);
330 		}
331 
332 		gtk_widget_show(item);
333 		gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item);
334 	}
335 
336 	gtk_menu_item_set_submenu(GTK_MENU_ITEM(main_menu_item), submenu);
337 	gtk_container_add(GTK_CONTAINER(geany->main_widgets->tools_menu),
338 									main_menu_item);
339 
340 	/* Setup keybindings. */
341 	key_group = plugin_set_key_group
342 		(plugin, "Line Operations", G_N_ELEMENTS(menu_items), NULL);
343 	for (i = 0; i < G_N_ELEMENTS(menu_items); i++)
344 	{
345 		if (menu_items[i].label != NULL)
346 		{
347 			keybindings_set_item(key_group, i,
348 				lo_keybinding_callback, 0, 0, menu_items[i].kb_section_name,
349 					menu_items[i].label, NULL);
350 		}
351 	}
352 
353 	return TRUE;
354 }
355 
356 
357 /* Show help */
358 static void
lo_help(G_GNUC_UNUSED GeanyPlugin * plugin,G_GNUC_UNUSED gpointer pdata)359 lo_help (G_GNUC_UNUSED GeanyPlugin *plugin, G_GNUC_UNUSED gpointer pdata)
360 {
361 	utils_open_browser("https://plugins.geany.org/lineoperations.html");
362 }
363 
364 
365 static void
lo_cleanup(GeanyPlugin * plugin,gpointer pdata)366 lo_cleanup(GeanyPlugin *plugin, gpointer pdata)
367 {
368 	gtk_widget_destroy(main_menu_item);
369 	lo_free_info();
370 }
371 
372 
373 G_MODULE_EXPORT
geany_load_module(GeanyPlugin * plugin)374 void geany_load_module(GeanyPlugin *plugin)
375 {
376 	main_locale_init(LOCALEDIR, GETTEXT_PACKAGE);
377 
378 	plugin->info->name        = _("Line Operations");
379 	plugin->info->description = _("Line Operations provides a handful of functions that can be applied to a document or selection such as, removing duplicate lines, removing empty lines, removing lines with only whitespace, and sorting lines.");
380 	plugin->info->version     = "0.3";
381 	plugin->info->author      = "Sylvan Mostert <smostert.dev@gmail.com>";
382 
383 	plugin->funcs->init       = lo_init;
384 	plugin->funcs->cleanup    = lo_cleanup;
385 	plugin->funcs->configure  = lo_configure;
386 	plugin->funcs->help       = lo_help;
387 
388 	GEANY_PLUGIN_REGISTER(plugin, 225);
389 }
390