Plan 9 from Bell Labs’s /sys/src/libcontrol/string.c

Copyright © 2021 Plan 9 Foundation
Distributed under the MIT License.
Download the Plan 9 distribution.


#include <u.h>
#include <libc.h>
#include <thread.h>
#include <draw.h>
#include <mouse.h>
#include <keyboard.h>
#include <control.h>
#include <editcontrol.h>
#include "string.h"

static struct {
	char *name;
	Stringset *ss;
} *stringsets;
int nstringset;

static String *	stringcow(String *s);	// cow: copy on write
static Undo *	_newundo(Stringset *ss);

Stringset *
stringsetnamed(char *name)
{
	int i;

	for (i = 0; i < nstringset; i++)
		if (strcmp(stringsets[i].name, name) == 0)
			return(stringsets[i].ss);
	return nil;
}

Stringset *
newstringset(char *name)
{
	Stringset *ss;

	if (stringsetnamed(name))
		ctlerror("namestringset: %s exists", name);
	ss = ctlmalloc(sizeof(Stringset));
	ss->string = nil;
	ss->nstring = 0;
	ss->nalloc = 0;
	ss->snarf = nil;
	ss->nsnarf = 0;
	ss->undostate = &ss->undohome;
	ss->undohome.next = nil;
	ss->undohome.prev = nil;
	stringsets = ctlrealloc(stringsets, (nstringset+1)*sizeof(stringsets[0]));
	stringsets[nstringset].ss = ss;
	stringsets[nstringset++].name = strdup(name);
	return ss;
}

void
freestringset(Stringset *ss)
{
	int i;
	Undo *u, *unext;

	if (ss == nil)
		return;
	for (i = 0; i < ss->nstring; i++)
		freestring(ss->string[i]);
	free(ss->string);
	for (i = 0; i < ss->nsnarf; i++)
		freestring(ss->snarf[i]);
	free(ss->snarf);
	for (u = ss->undohome.next; u; u = unext){
		freestring(u->s);
		unext = u->next;
		free(u);
	}
	free(ss);
}

void
snarfclear(Stringset *ss)
{
	int i;

	// remove old snarf buffer
	for (i = 0; i < ss->nsnarf; i++)
		freestring(ss->snarf[i]);
	ss->nsnarf = 0;
}

String*
stringof(Stringset *ss, Position pos)
{
	if (ss->string == nil || pos.str < 0 || pos.str >= ss->nstring)
		return nil;
	return ss->string[pos.str];
}

Context *
_contextnamed(char *s)
{
	Context *c;

	for (c = context; c; c = c->next)
		if (strcmp(s, c->name) == 0)
			return c;
	return nil;
}

int
_posbefore(Position p1, Position p2)
{
	return p1.str < p2.str || (p1.str == p2.str && p1.rn < p2.rn);
}

int
_posequal(Position p1, Position p2)
{
	return p1.str == p2.str && p1.rn == p2.rn;
}

int
_posdec(Stringset *ss, Position *pp)
{
	String *s;
	Position p;

	p = *pp;
	while (p.rn == 0){
		if (p.str-- == 0 || (s = stringof(ss, p)) == nil)
			return 0;
		p.rn = s->n;
	}
	p.rn--;
	*pp = p;
	return 1;
}

int
_posinc(Stringset *ss, Position *pp)
{
	String *s;
	Position p;

	p = *pp;
	while ((s = stringof(ss, p)) && p.rn == s->n){
		p.str++;
		p.rn = 0;
	}
	if (s == nil)
		return 0;
	p.rn++;
	*pp = p;
	return 1;
}

int
_stringfindrune(Stringset *ss, Position *pp, Rune r)
{
	String *s;
	Position pos;

	s = nil;
	pos = *pp;
	for(;;){
		if (s){
			if (pos.rn < s->n){
				if (s->r[pos.rn] == r)
					break;
				else
					pos.rn++;
			}else{
				pos.str++;
				pos.rn = 0;
				s = nil;
			}
		}else if ((s = stringof(ss, pos)) == nil)
			return 0;
	}
	*pp = pos;
	return 1;
}

int
_stringfindrrune(Stringset *ss, Position *pp, Rune r)
{
	String *s;
	Position pos;

	s = nil;
	pos = *pp;
	for(;;){
		if (pos.rn < 0){
			if (--pos.str < 0)
				return 0;
			else{
				if (s = stringof(ss, pos)){
					pos.rn = s->n - 1;
				}else
					return 0;
			}
		} else {
			if (s){
				if (s->r[pos.rn] == r)
					break;
				else
					pos.rn--;
			}else if ((s = stringof(ss, pos)) == nil)
				return 0;
		}
	}
	*pp = pos;
	return 1;
}

void
_stringspace(Stringset *ss, int n)
{
	if (n <= ss->nalloc)
		return;
	debugprint(DBGSTRING, "stringspace: %d\n", n);
	ss->string = ctlrealloc(ss->string, n * sizeof(String*));
	memset(ss->string+ss->nalloc, 0, (n-ss->nalloc)*sizeof(String*));
	ss->nalloc = n;
}

void
_posadjust(Undo* c, Position *p)
{
	/* BEWARE: _posadjust is called AFTER the operation
	 * was inverted.  Therefore, the adjustment for a delete
	 * operation is found under the Insert case below
	 */
	debugprint(DBGPOSITION, "posperform: %d/%d ", p->str, p->rn);
	switch(c->op){
	case Delete:
		if(p->str >= c->p.str)
			p->str++;
		debugprint(DBGPOSITION, "insert %d: %d/%d\n",
			c->p.str, p->str, p->rn);
		break;
	case Insert:
		if (p->str == c->p.str)
			p->rn = 0;
		if(p->str > c->p.str)
			p->str--;
		debugprint(DBGPOSITION, "delete %d: %d/%d\n",
			c->p.str, p->str, p->rn);
		break;
	case Join:
		if(p->str > c->p.str)
			p->str++;
		else if (p->str == c->p.str && p->rn >= c->p.rn){
			p->rn -= c->p.rn;
			p->str++;
		}
		debugprint(DBGPOSITION, "split at %d/%d: %d/%d\n",
			c->p.str, c->p.rn, p->str, p->rn);
		break;
	case Split:
		if (p->str == c->p.str+1)
			p->rn += c->p.rn;
		if(p->str > c->p.str)
			p->str--;
		debugprint(DBGPOSITION, "join at %d/%d: %d/%d\n",
			c->p.str, c->p.rn, p->str, p->rn);
		break;
	}
}

void
perform(Stringset *ss, Undo* u)
{
	String *s;
	Position p;

	switch(u->op){
	case Insert:
		debugprint(DBGPERFORM, "insert: %d runes, context %s at line %d\n",
			u->s->n, u->s->context, u->p.str);
		if (ss->c)
			layoutprep((Edit*)ss->c, u->p, u->p);
		_stringspace(ss, ss->nstring + 1);
		memmove(ss->string + u->p.str + 1, ss->string + u->p.str, (ss->nstring - u->p.str)*sizeof(String*));
		ss->nstring++;
		ss->string[u->p.str] = u->s;
		if (u->s){
			u->s->ref++;
			debugprint(DBGSTRING, "perform: 0x%p ref++ → %d\n", u->s, u->s->ref);
		}
		break;
	case Delete:
		debugprint(DBGPERFORM, "delete: %d runes, context %s at line %d\n",
			u->s->n, u->s->context, u->p.str);
		p = u->p;
		p.str++;
		p.rn = 0;
		if (ss->c)
			layoutprep((Edit*)ss->c, u->p, p);
		freestring(ss->string[u->p.str]);
		ss->nstring--;
		memmove(ss->string + u->p.str, ss->string + u->p.str + 1, (ss->nstring - u->p.str)*sizeof(String*));
		break;
	case Split:
		debugprint(DBGPERFORM, "split: at position %d/%d\n",
			u->p.str, u->p.rn);
		p = u->p;
		p.str++;
		p.rn = 0;
		if (ss->c)
			layoutprep((Edit*)ss->c, u->p, p);
		_stringspace(ss, ss->nstring + 1);
		memmove(ss->string + u->p.str + 1, ss->string + u->p.str, (ss->nstring - u->p.str)*sizeof(String*));
		ss->nstring++;
		ss->string[u->p.str] = s = stringcow(ss->string[u->p.str]);
		ss->string[u->p.str+1] = _allocstring(s->context, s->r + u->p.rn, s->n - u->p.rn);
		s->n = u->p.rn;
		break;
	case Join:
		debugprint(DBGPERFORM, "join: strings %d and %d\n",
			u->p.str, u->p.str+1);
		p = u->p;
		p.str += 2;
		p.rn = 0;
		if (ss->c)
			layoutprep((Edit*)ss->c, u->p, p);
		ss->string[u->p.str] = s = stringcow(ss->string[u->p.str]);
		s->r = ctlrealloc(s->r, (s->n + ss->string[u->p.str+1]->n) * sizeof(Rune));
		memmove(s->r + s->n, ss->string[u->p.str+1]->r, ss->string[u->p.str+1]->n * sizeof(Rune));
		s->n += ss->string[u->p.str+1]->n;
		freestring(ss->string[u->p.str+1]);
		ss->nstring--;
		memmove(ss->string + u->p.str+1, ss->string + u->p.str + 2, (ss->nstring - u->p.str-1)*sizeof(String*));
		break;
	}
	u->op ^= 1;
	if (ss->c){
		adjustselections((Edit*)ss->c, u);
		adjustlines((Edit*)ss->c, u);
	}
}

void
split(Stringset *ss, Position p)
{
	Undo *u;

	u = _newundo(ss);
	u->op = Split;
	u->p = p;
	perform(ss, u);
}

void
join(Stringset *ss, Position p)
{
	Undo *u;

	u = _newundo(ss);
	u->op = Join;
	u->p = p;
	perform(ss, u);
}

void
insert(Stringset *ss, int n, String *s)
{
	Undo *u;

	u = _newundo(ss);
	u->op = Insert;
	u->p.str = n;
	u->p.rn = 0;
	u->s = s;
	if (u->s){
		u->s->ref++;
		debugprint(DBGSTRING, "insert: 0x%p ref++ → %d\n", u->s, u->s->ref);
	}
	perform(ss, u);
}

void
delete(Stringset *ss, int n)
{
	Undo *u;

	u = _newundo(ss);
	u->op = Delete;
	u->p.str = n;
	u->p.rn = 0;
	u->s = stringof(ss, u->p);
	if (u->s){
		u->s->ref++;
		debugprint(DBGSTRING, "delete: 0x%p ref++ → %d\n", u->s, u->s->ref);
	}
	perform(ss, u);
}

void
undobegingroup(Stringset *ss)
{
	if (ss->undogrouplevel++ == 0)
		ss->undobegin = ss->undostate;
}

void
undoendgroup(Stringset *ss)
{
	Undo *u;

	if (ss->undogrouplevel <= 0)
		ctlerror("undoendgroup: unbegun");
	if (--ss->undogrouplevel > 0)
		return;
	for (u = ss->undobegin->next; u != ss->undostate; u = u->next){
		if (u == nil)
			ctlerror("undoendgroup: at end of undo list");
		u->flags |= Nextalso;
	}
	ss->undobegin = nil;
}


void
undo(Stringset *ss)
{
	if (ss->undogrouplevel)
		ctlerror("undo: group undo in progress");
	if (ss->undostate == &ss->undohome){
		debugprint(DBGPERFORM, "undo: nothing to undo\n");
		return;	/* nothing to undo */
	}
	do {
		assert(ss->undostate != &ss->undohome);
		perform(ss, ss->undostate);
		ss->undostate = ss->undostate->prev;
	} while(ss->undostate->flags & Nextalso);
}

void
redo(Stringset *ss)
{
	if (ss->undogrouplevel)
		ctlerror("redo: group undo in progress");
	if (ss->undostate->next == nil)
		return;	/* nothing to redo */
	do {
		assert(ss->undostate != nil);
		ss->undostate = ss->undostate->next;
		perform(ss, ss->undostate);
	} while(ss->undostate->flags & Nextalso);
}

void
cut(Stringset *ss, Position p1, Position p2)
{
	String *s;
	int n;

	if (_posequal(p1, p2))
		return;
	assert(_posbefore(p1, p2));
	if (debug & DBGCUT)
		dumpstrings(ss);

	undobegingroup(ss);
	// start at the end to avoid compromising positions by changes
	s = stringof(ss, p2);
	if (s && p2.rn > 0 && p2.rn < s->n){
		split(ss, p2);
	}else if (p2.rn == 0)
		p2.str--;
	for (n = p2.str; n > p1.str; n--)
		delete(ss, n);
	s = stringof(ss, p1);
	if (s && p1.rn > 0 && p1.rn < s->n){
		split(ss, p1);
		delete(ss, p1.str+1);
	}else if (p1.rn == 0)
		delete(ss, p1.str);
	undoendgroup(ss);
	if (debug & DBGCUT)
		dumpstrings(ss);
}

void
snarf(Stringset *ss, Position p1, Position p2)
{
	String *s;
	int i;

	if (_posequal(p1, p2))
		return;
	assert(_posbefore(p1, p2));
	snarfclear(ss);

	debugprint(DBGSTRING, "snarf: %d/%d — %d/%d\n", p1.str, p1.rn, p2.str, p2.rn);

	undobegingroup(ss);
	if (ss->undostate != &ss->undohome)
		ss->undobegin = ss->undobegin->prev;	// combine with previous undo

	s = stringof(ss, p1);
	if (s && p1.rn > 0 && p1.rn < s->n){
		debugprint(DBGSTRING, "snarf: split at p1: %d/%d — %d/%d\n",
			p1.str, p1.rn, p2.str, p2.rn);
		split(ss, p1);
		p1.str++;
		_posadjust(ss->undostate, &p2);
		debugprint(DBGSTRING, "snarf: after split at p1: %d/%d — %d/%d\n",
			p1.str, p1.rn, p2.str, p2.rn);
	}else if (p1.rn == s->n)
		p1.str++;

	s = stringof(ss, p2);
	if (s && p2.rn > 0 && p2.rn < s->n)
		split(ss, p2);
	else if (p2.rn == 0)
		p2.str--;

	ss->nsnarf = p2.str - p1.str + 1;
	ss->snarf = ctlrealloc(ss->snarf, ss->nsnarf*sizeof(String*));
	for (i = p1.str; i <= p2.str; i++){
		debugprint(DBGSTRING, "snarf: copy string: %d 0x%p\n",
			i, ss->string[i]);
		ss->snarf[i-p1.str] = ss->string[i];
		if (ss->string[i]) ss->string[i]->ref++;
	}
	undoendgroup(ss);
	if (ss->c)
		chanprint(ss->c->event, "%q: snarf %d", ss->c->name, ss->nsnarf);
}

void
_stringadd(Stringset *ss, String *s, int n)
{
	int i;

	debugprint(DBGUSER, "addstring %d\n", n);
	if (n >= ss->nstring){
		debugprint(DBGUSER, "addstring: allocating string %d\n", n);
		ss->string = ctlrealloc(ss->string, (n+1)*sizeof(String*));
		for (i = ss->nstring; i < n; i++)
			ss->string[i] = nil;
		ss->nstring = n+1;
	}else if (ss->string[n]){
		debugprint(DBGUSER, "removing string %d\n", n);
		freestring(ss->string[n]);
		ss->string[n] = nil;
	}
	ss->string[n] = s;
	s->ref++;
}

void
paste(Stringset *ss, Position pos, String **st, int ns)
{
	String *s;
	int i;

	undobegingroup(ss);
	s = stringof(ss, pos);
	if (s == nil || pos.rn < 0 || pos.rn > s->n){
		if (ss->c)
			ctlerror("%q: paste in illegal position: %d/%d", ss->c->name, pos.str, pos.rn);
		else
			ctlerror("paste in illegal position: %d/%d", pos.str, pos.rn);
	}
	if (pos.rn){
		if (pos.rn < s->n){
			debugprint(DBGPASTE, "paste: middle of string\n");
			split(ss, pos);
		}else
			debugprint(DBGPASTE, "paste: at end of string\n");
		pos.str++;
	}
	for (i = 0; i < ns; i++)
		insert(ss, pos.str+i, st[i]);
	undoendgroup(ss);
}

Position
insertrunes(Stringset *ss, Position p, Rune *rp, int nr)
{
	String *s;

	s = stringof(ss, p);
	if (s == nil){
		if (ss->c)
			ctlerror("%q: insert into non string at position %d/%d", ss->c->name, p.str, p.rn);
		else
			ctlerror("insert into non string at position %d/%d", p.str, p.rn);
	}
	s = _allocstring(s->context, rp, nr);
	paste(ss, p, &s, 1);
	freestring(s);
	p.str++;
	p.rn = 0;
	return p;
}

Position
_addonerune(Stringset *ss, Position pos, Rune rune)
{
	String *s;

	if ((s = stringof(ss, pos))	// pos points into existing string
		&& pos.rn == s->n	// at its end
		&& ss->undostate != &ss->undohome	// there's something to undo
		&& ss->undostate->op == Delete	// current undo is a delete
		&& ss->undostate->p.str == pos.str	// refers to current string
		&& ss->undostate->s == s	// and (still) uses the same copy of it
		&& s->ref == 2	// and these strings are not used elsewhere
	){
		/* Just add rune to the end of current String, which automatically
		 * adds it to current Undo as well
		 */
		debugprint(DBGCUT|DBGPASTE, "addonerune: optimized case\n");
		if (ss->c)
			layoutprep((Edit*)ss->c, pos, pos);
		s->r = ctlrealloc(s->r, (s->n+1)*sizeof(Rune));
		s->r[s->n++] = rune;
		pos.rn = s->n;
		return pos;
	}
	debugprint(DBGCUT|DBGPASTE, "addonerune: normal case\n");
	return insertrunes(ss, pos, &rune, 1);
}

Position
_delonerune(Stringset *ss, Position pos)
{
	String *s;
	Position p2;

	p2 = pos;
	if ((s = stringof(ss, pos))	// points into existing string
		&& s->n > 1	// it's got more than one character
		&& pos.rn == s->n	// we're at its end
		&& ss->undostate != &ss->undohome	// there's something to undo
		&& ss->undostate->op == Delete	// current undo is a delete
		&& ss->undostate->p.str == pos.str	// refers to current string
		&& ss->undostate->s == s	// and (still) uses the same copy of it
		&& s->ref == 2	// and these strings are not used elsewhere
	){
		/* Just delete a rune from the end of current String, which automatically
		 * incorporates it into current Undo
		 */
		debugprint(DBGCUT|DBGPASTE, "delonerune: optimized case\n");
		pos.rn--;
		if (ss->c)
			layoutprep((Edit*)ss->c, pos, p2);
		s->n--;
		return pos;
	}
	debugprint(DBGCUT|DBGPASTE, "delonerune: normal case\n");
	// backup one character
	if (pos.rn)
		pos.rn--;
	else for(;;) {
		String *s;

		if (pos.str == 0 ||  ss->string[pos.str-1] == nil){
			debugprint(DBGCUT|DBGPASTE, "delonerune: beginning of file\n");
			return pos;
		}
		s = ss->string[pos.str-1];
		if (s->n){
			pos.str--;
			pos.rn = s->n-1;
			p2.str = pos.str;
			p2.rn = s->n;
			break;
		}
	}
	cut(ss, pos, p2);
	return pos;
}

void
freestring(String *s)
{
	debugprint(DBGSTRING, "freestring: 0x%p ref %d [%d]", s, s?s->ref:0, s?s->n:0);
	if (s && (debug & DBGSTRING)){
		int i;
		for (i = 0; i < s->n; i++)fprint(2, "%C", s->r[i]);
		fprint(2, "\n");
	}
	if (s == nil || --s->ref > 0)
		return;
	if (s->ref < 0)
		abort();
	free(s->context);
	free(s->r);
	free(s);
}

String *
_allocstring(char *c, Rune *r, int n)
{
	String *s;

	s = ctlmalloc(sizeof(String));
	s->ref = 1;
	s->context = strdup(c);
	if (s->n = n){
		s->r = ctlmalloc(n*sizeof(Rune));
		memmove(s->r, r, n*sizeof(Rune));
	}else
		s->r = nil;
	debugprint(DBGSTRING, "_allocstring: 0x%p %s [%d]", s, c, n);
	if (debug & DBGSTRING){
		int i;
		for (i = 0; i < n; i++)fprint(2, "%C", r[i]);
		fprint(2, "\n");
	}
	return s;
}

String *
allocstring(char *context, char *p)
{
	String *s;

	if (_contextnamed(context) == nil)
		ctlerror("allocstring: no such context %q", context);
	s = ctlmalloc(sizeof(String));
	s->ref = 1;
	s->context = strdup(context);
	s->r = _ctlrunestr(p);
	s->n = runestrlen(s->r);
	debugprint(DBGSTRING, "_allocstring: 0x%p %s [%d]", s, context, s->n);
	if (debug & DBGSTRING){
		int i;
		for (i = 0; i < s->n; i++) fprint(2, "%C", s->r[i]);
		fprint(2, "\n");
	}
	return s;
}

static String*
stringcow(String *s)	// cow: copy on write
{
	if (s->ref == 1)
		return s;
	--s->ref;
	assert(s->ref > 0);
	return _allocstring(s->context, s->r, s->n);
}

void
emptyundo(Stringset *ss)
{
	Undo *u;

	if (ss->undobegin)
		ctlerror("emptyundo: group undo in progress");
	while ((u = ss->undostate) != &ss->undohome){
		freestring(u->s);
		ss->undostate = u->prev;
		ss->undostate->next = u->next;
		free(u);
	}
}

static Undo *
_newundo(Stringset *ss)
{
	Undo *u;

	if (ss->undostate->next == nil){
		ss->undostate->next = u = ctlmalloc(sizeof(Undo));
		u->prev = ss->undostate;
		u->next = nil;
		u->op = Noop;
		u->flags = 0;
		u->s = nil;
	}else{
		for (u = ss->undostate->next; u && u->op != Noop; u = u->next){
			freestring(u->s);
			u->s = nil;
			u->op = Noop;
			u->flags = 0;
		}
	}
	ss->undostate = ss->undostate->next;
	return ss->undostate;
}

Context *
_newcontext(char *name)
{
	Context *c;

	if (_contextnamed(name))
		ctlerror("newcontext: context %s exists", name);
	c = ctlmalloc(sizeof(Context));
	c->next = context;
	context = c;
	c->name = strdup(name);
	c->f = nil;
	c->wordbreak = 0;
	c->tabs[0] = -1;
	
	return c;
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to webmaster@plan9.bell-labs.com.