1/*	$OpenBSD: paragraph.c,v 1.49 2023/04/21 13:39:37 op Exp $	*/
2
3/* This file is in the public domain. */
4
5/*
6 * Code for dealing with paragraphs and filling. Adapted from MicroEMACS 3.6
7 * and GNU-ified by mwm@ucbvax.	 Several bug fixes by blarson@usc-oberon.
8 */
9
10#include <sys/queue.h>
11#include <ctype.h>
12#include <limits.h>
13#include <signal.h>
14#include <stdio.h>
15#include <stdlib.h>
16
17#include "def.h"
18
19static int	fillcol = 70;
20
21#define MAXWORD 256
22
23static int	findpara(void);
24static int 	do_gotoeop(int, int, int *);
25
26/*
27 * Move to start of paragraph.
28 * Move backwards by line, checking from the 1st character forwards for the
29 * existence a non-space. If a non-space character is found, move to the
30 * preceding line. Keep doing this until a line with only spaces is found or
31 * the start of buffer.
32 */
33int
34gotobop(int f, int n)
35{
36	int col, nospace;
37
38	/* the other way... */
39	if (n < 0)
40		return (gotoeop(f, -n));
41
42	while (n-- > 0) {
43		nospace = 0;
44		while (lback(curwp->w_dotp) != curbp->b_headp) {
45			curwp->w_doto = 0;
46			col = 0;
47
48			while (col < llength(curwp->w_dotp) &&
49			    (isspace(lgetc(curwp->w_dotp, col))))
50				col++;
51
52			if (col >= llength(curwp->w_dotp)) {
53				if (nospace)
54					break;
55			} else
56				nospace = 1;
57
58			curwp->w_dotline--;
59			curwp->w_dotp = lback(curwp->w_dotp);
60		}
61	}
62	/* force screen update */
63	curwp->w_rflag |= WFMOVE;
64	return (TRUE);
65}
66
67/*
68 * Move to end of paragraph.
69 * See comments for gotobop(). Same, but moving forwards.
70 */
71int
72gotoeop(int f, int n)
73{
74	int i;
75
76	return(do_gotoeop(f, n, &i));
77}
78
79int
80do_gotoeop(int f, int n, int *i)
81{
82	int col, nospace, j = 0;
83
84	/* the other way... */
85	if (n < 0)
86		return (gotobop(f, -n));
87
88	/* for each one asked for */
89	while (n-- > 0) {
90		*i = ++j;
91		nospace = 0;
92		while (lforw(curwp->w_dotp) != curbp->b_headp) {
93			col = 0;
94			curwp->w_doto = 0;
95
96			while (col < llength(curwp->w_dotp) &&
97			    (isspace(lgetc(curwp->w_dotp, col))))
98				col++;
99
100			if (col >= llength(curwp->w_dotp)) {
101				if (nospace)
102					break;
103			} else
104				nospace = 1;
105
106			curwp->w_dotp = lforw(curwp->w_dotp);
107			curwp->w_dotline++;
108
109		}
110	}
111	/* do not continue after end of buffer */
112	if (lforw(curwp->w_dotp) == curbp->b_headp) {
113		gotoeol(FFRAND, 1);
114		curwp->w_rflag |= WFMOVE;
115		return (FALSE);
116	}
117
118	/* force screen update */
119	curwp->w_rflag |= WFMOVE;
120	return (TRUE);
121}
122
123/*
124 * Justify a paragraph.  Fill the current paragraph according to the current
125 * fill column.
126 */
127int
128fillpara(int f, int n)
129{
130	int	 c;		/* current char during scan		*/
131	int	 wordlen;	/* length of current word		*/
132	int	 clength;	/* position on line during fill		*/
133	int	 i;		/* index during word copy		*/
134	int	 eopflag;	/* Are we at the End-Of-Paragraph?	*/
135	int	 firstflag;	/* first word? (needs no space)		*/
136	int	 newlength;	/* tentative new line length		*/
137	int	 eolflag;	/* was at end of line			*/
138	int	 retval;	/* return value				*/
139	struct line	*eopline;	/* pointer to line just past EOP	*/
140	char	 wbuf[MAXWORD];	/* buffer for current word		*/
141
142	if (n == 0)
143		return (TRUE);
144
145	undo_boundary_enable(FFRAND, 0);
146
147	/* record the pointer to the line just past the EOP */
148	(void)gotoeop(FFRAND, 1);
149	if (curwp->w_doto != 0) {
150		/* paragraph ends at end of buffer */
151		(void)lnewline();
152		eopline = lforw(curwp->w_dotp);
153	} else
154		eopline = curwp->w_dotp;
155
156	/* and back top the beginning of the paragraph */
157	(void)gotobop(FFRAND, 1);
158
159	/* initialize various info */
160	while (inword() == 0 && forwchar(FFRAND, 1));
161
162	clength = curwp->w_doto;
163	wordlen = 0;
164
165	/* scan through lines, filling words */
166	firstflag = TRUE;
167	eopflag = FALSE;
168	while (!eopflag) {
169
170		/* get the next character in the paragraph */
171		if ((eolflag = (curwp->w_doto == llength(curwp->w_dotp)))) {
172			c = ' ';
173			if (lforw(curwp->w_dotp) == eopline)
174				eopflag = TRUE;
175		} else
176			c = lgetc(curwp->w_dotp, curwp->w_doto);
177
178		/* and then delete it */
179		if (ldelete((RSIZE) 1, KNONE) == FALSE && !eopflag) {
180			retval = FALSE;
181			goto cleanup;
182		}
183
184		/* if not a separator, just add it in */
185		if (c != ' ' && c != '\t') {
186			if (wordlen < MAXWORD - 1)
187				wbuf[wordlen++] = c;
188			else {
189				/*
190				 * You lose chars beyond MAXWORD if the word
191				 * is too long. I'm too lazy to fix it now; it
192				 * just silently truncated the word before,
193				 * so I get to feel smug.
194				 */
195				ewprintf("Word too long!");
196			}
197		} else if (wordlen) {
198
199			/* calculate tentative new length with word added */
200			newlength = clength + 1 + wordlen;
201
202			/*
203			 * if at end of line or at doublespace and previous
204			 * character was one of '.','?','!' doublespace here.
205			 * behave the same way if a ')' is preceded by a
206			 * [.?!] and followed by a doublespace.
207			 */
208			if (dblspace && (!eopflag && ((eolflag ||
209			    curwp->w_doto == llength(curwp->w_dotp) ||
210			    (c = lgetc(curwp->w_dotp, curwp->w_doto)) == ' '
211			    || c == '\t') && (ISEOSP(wbuf[wordlen - 1]) ||
212			    (wbuf[wordlen - 1] == ')' && wordlen >= 2 &&
213			    ISEOSP(wbuf[wordlen - 2])))) &&
214			    wordlen < MAXWORD - 1))
215				wbuf[wordlen++] = ' ';
216
217			/* at a word break with a word waiting */
218			if (newlength <= fillcol) {
219				/* add word to current line */
220				if (!firstflag) {
221					(void)linsert(1, ' ');
222					++clength;
223				}
224				firstflag = FALSE;
225			} else {
226				if (curwp->w_doto > 0 &&
227				    lgetc(curwp->w_dotp, curwp->w_doto - 1) == ' ') {
228					curwp->w_doto -= 1;
229					(void)ldelete((RSIZE) 1, KNONE);
230				}
231				/* start a new line */
232				(void)lnewline();
233				clength = 0;
234			}
235
236			/* and add the word in in either case */
237			for (i = 0; i < wordlen; i++) {
238				(void)linsert(1, wbuf[i]);
239				++clength;
240			}
241			wordlen = 0;
242		}
243	}
244	/* and add a last newline for the end of our new paragraph */
245	(void)lnewline();
246
247	/*
248	 * We really should wind up where we started, (which is hard to keep
249	 * track of) but I think the end of the last line is better than the
250	 * beginning of the blank line.
251	 */
252	(void)backchar(FFRAND, 1);
253	retval = TRUE;
254cleanup:
255	undo_boundary_enable(FFRAND, 1);
256	return (retval);
257}
258
259/*
260 * Delete n paragraphs. Move to the beginning of the current paragraph, or if
261 * the cursor is on an empty line, move down the buffer to the first line with
262 * non-space characters. Then mark n paragraphs and delete.
263 */
264int
265killpara(int f, int n)
266{
267	int	lineno, status;
268
269	if (n == 0)
270		return (TRUE);
271
272	if (findpara() == FALSE)
273		return (TRUE);
274
275	/* go to the beginning of the paragraph */
276	(void)gotobop(FFRAND, 1);
277
278	/* take a note of the line number for after deletions and set mark */
279	lineno = curwp->w_dotline;
280	curwp->w_markp = curwp->w_dotp;
281	curwp->w_marko = curwp->w_doto;
282
283	(void)gotoeop(FFRAND, n);
284
285	if ((status = killregion(FFRAND, 1)) != TRUE)
286		return (status);
287
288	curwp->w_dotline = lineno;
289	return (TRUE);
290}
291
292/*
293 * Mark n paragraphs starting with the n'th and working our way backwards.
294 * This leaves the cursor at the beginning of the paragraph where markpara()
295 * was invoked.
296 */
297int
298markpara(int f, int n)
299{
300	int i = 0;
301
302	if (n == 0)
303		return (TRUE);
304
305	clearmark(FFARG, 0);
306
307	if (findpara() == FALSE)
308		return (TRUE);
309
310	(void)do_gotoeop(FFRAND, n, &i);
311
312	/* set the mark here */
313	curwp->w_markp = curwp->w_dotp;
314	curwp->w_marko = curwp->w_doto;
315
316	(void)gotobop(FFRAND, i);
317
318	return (TRUE);
319}
320
321/*
322 * Transpose the current paragraph with the following paragraph. If invoked
323 * multiple times, transpose to the n'th paragraph. If invoked between
324 * paragraphs, move to the previous paragraph, then continue.
325 */
326int
327transposepara(int f, int n)
328{
329	int	i = 0, status;
330	char	flg;
331
332	if (n == 0)
333		return (TRUE);
334
335	undo_boundary_enable(FFRAND, 0);
336
337	/* find a paragraph, set mark, then goto the end */
338	gotobop(FFRAND, 1);
339	curwp->w_markp = curwp->w_dotp;
340	curwp->w_marko = curwp->w_doto;
341	(void)gotoeop(FFRAND, 1);
342
343	/* take a note of buffer flags - we may need them */
344	flg = curbp->b_flag;
345
346	/* clean out kill buffer then kill region */
347	kdelete();
348	if ((status = killregion(FFRAND, 1)) != TRUE)
349		return (status);
350
351	/*
352	 * Now step through n paragraphs. If we reach the end of buffer,
353	 * stop and paste the killed region back, then display a message.
354	 */
355	if (do_gotoeop(FFRAND, n, &i) == FALSE) {
356		ewprintf("Cannot transpose paragraph, end of buffer reached.");
357		(void)gotobop(FFRAND, i);
358		(void)yank(FFRAND, 1);
359		curbp->b_flag = flg;
360		return (FALSE);
361	}
362	(void)yank(FFRAND, 1);
363
364	undo_boundary_enable(FFRAND, 1);
365
366	return (TRUE);
367}
368
369/*
370 * Go down the buffer until we find a line with non-space characters.
371 */
372int
373findpara(void)
374{
375	int	col, nospace = 0;
376
377	/* we move forward to find a para to mark */
378	do {
379		curwp->w_doto = 0;
380		col = 0;
381
382		/* check if we are on a blank line */
383		while (col < llength(curwp->w_dotp)) {
384			if (!isspace(lgetc(curwp->w_dotp, col)))
385				nospace = 1;
386			col++;
387		}
388		if (nospace)
389			break;
390
391		if (lforw(curwp->w_dotp) == curbp->b_headp)
392			return (FALSE);
393
394		curwp->w_dotp = lforw(curwp->w_dotp);
395		curwp->w_dotline++;
396	} while (1);
397
398	return (TRUE);
399}
400
401/*
402 * Insert char with work wrap.  Check to see if we're past fillcol, and if so,
403 * justify this line.  As a last step, justify the line.
404 */
405int
406fillword(int f, int n)
407{
408	char	c;
409	int	col, i, nce;
410
411	for (i = col = 0; col <= fillcol; ++i, ++col) {
412		if (i == curwp->w_doto)
413			return selfinsert(f, n);
414		c = lgetc(curwp->w_dotp, i);
415		if (c == '\t')
416			col = ntabstop(col, curwp->w_bufp->b_tabw);
417		else if (ISCTRL(c) != FALSE)
418			++col;
419	}
420	if (curwp->w_doto != llength(curwp->w_dotp)) {
421		(void)selfinsert(f, n);
422		nce = llength(curwp->w_dotp) - curwp->w_doto;
423	} else
424		nce = 0;
425	curwp->w_doto = i;
426
427	if ((c = lgetc(curwp->w_dotp, curwp->w_doto)) != ' ' && c != '\t')
428		do {
429			(void)backchar(FFRAND, 1);
430		} while ((c = lgetc(curwp->w_dotp, curwp->w_doto)) != ' ' &&
431		    c != '\t' && curwp->w_doto > 0);
432
433	if (curwp->w_doto == 0)
434		do {
435			(void)forwchar(FFRAND, 1);
436		} while ((c = lgetc(curwp->w_dotp, curwp->w_doto)) != ' ' &&
437		    c != '\t' && curwp->w_doto < llength(curwp->w_dotp));
438
439	(void)delwhite(FFRAND, 1);
440	(void)lnewline();
441	i = llength(curwp->w_dotp) - nce;
442	curwp->w_doto = i > 0 ? i : 0;
443	curwp->w_rflag |= WFMOVE;
444	if (nce == 0 && curwp->w_doto != 0)
445		return (fillword(f, n));
446	return (TRUE);
447}
448
449/*
450 * Set fill column to n for justify.
451 */
452int
453setfillcol(int f, int n)
454{
455	char buf[32], *rep;
456	const char *es;
457	int nfill;
458
459	if ((f & FFARG) != 0) {
460		fillcol = n;
461	} else {
462		if ((rep = eread("Set fill-column: ", buf, sizeof(buf),
463		    EFNEW | EFCR)) == NULL)
464			return (ABORT);
465		else if (rep[0] == '\0')
466			return (FALSE);
467		nfill = strtonum(rep, 0, INT_MAX, &es);
468		if (es != NULL) {
469			dobeep();
470			ewprintf("Invalid fill column: %s", rep);
471			return (FALSE);
472		}
473		fillcol = nfill;
474		ewprintf("Fill column set to %d", fillcol);
475	}
476	return (TRUE);
477}
478
479int
480sentencespace(int f, int n)
481{
482	if (f & FFARG)
483		dblspace = n > 1;
484	else
485		dblspace = !dblspace;
486
487	return (TRUE);
488}
489