#include #include #include #include #include #include #include #include "libframe/frame.h" #include #include #include #include #include "dat.h" #include "fns.h" Image* tagcols[NCOL]; Image* textcols[NCOL]; static Rune Ldot[] = {'.', 0}; enum { TABDIR = 3 /* width of tabs in directory windows */ }; void textinit(Text* t, File* f, Rectangle r, Reffont* rf, Image* cols[NCOL]) { t->file = f; t->all = r; t->scrollr = r; t->scrollr.max.x = r.min.x + Scrollwid; t->lastsr = nullrect; r.min.x += Scrollwid + Scrollgap; t->eq0 = ~0; t->ncache = 0; t->reffont = rf; t->tabstop = maxtab; memmove(t->fr.cols, cols, sizeof t->fr.cols); textredraw(t, r, rf->f, screen, -1, 0); } void _textsettick(Text* current, Text* self) { self->fr.currenttick = (self == current) ? self->fr.activetick : self->fr.tick; } void textsettick(Text* t, Row* r) { Column** allcolumns; Column* cptr; Window* wptr; int ncols, ccnt, wcnt; if (!bartflag) return; allcolumns = r->col; ncols = r->ncol; _textsettick(t, &r->tag); for (ccnt = 0; ccnt < ncols; ccnt++) { cptr = allcolumns[ccnt]; _textsettick(t, &cptr->tag); for (wcnt = 0; wcnt < cptr->nw; wcnt++) { wptr = cptr->w[wcnt]; /*if (!wptr->isdir) { winlock(wptr, 'M'); }*/ _textsettick(t, &wptr->tag); _textsettick(t, &wptr->body); /*if (!wptr->isdir) { winunlock(wptr); }*/ } } /* force redraw to refresh all ticks; * kind of expensive, but we only do it when the active frame changes */ rowresize(r, r->r); } void textredraw(Text* t, Rectangle r, Font* f, Image* b, int odx, int active) { int maxt; Rectangle rr; frinit(&t->fr, r, f, b, t->fr.cols); rr = t->fr.r; rr.min.x -= Scrollwid + Scrollgap; /* back fill to scroll bar */ if (!t->fr.noredraw) draw(t->fr.b, rr, t->fr.cols[BACK], nil, ZP); /* use no wider than 3-space tabs in a directory */ maxt = maxtab; if (t->what == Body) { if (t->w->isdir) maxt = min(TABDIR, maxtab); else maxt = t->tabstop; } t->fr.maxtab = maxt * stringwidth(f, "0"); if (t->what == Body && t->w->isdir && odx != Dx(t->all)) { if (t->fr.maxlines > 0) { textreset(t); textcolumnate(t, t->w->dlp, t->w->ndl); textshow(t, 0, 0, 1); } } else { textfill(t); textsetselect(t, t->q0, t->q1); } } int textresize(Text* t, Rectangle r, int keepextra) { int odx; if (Dy(r) <= 0) r.max.y = r.min.y; else if (!keepextra) r.max.y -= Dy(r) % t->fr.font->height; odx = Dx(t->all); t->all = r; t->scrollr = r; t->scrollr.max.x = r.min.x + Scrollwid; t->lastsr = nullrect; r.min.x += Scrollwid + Scrollgap; frclear(&t->fr, 0); textredraw(t, r, t->fr.font, t->fr.b, odx, 0); if (keepextra && t->fr.r.max.y < t->all.max.y && !t->fr.noredraw) { /* draw background in bottom fringe of window */ r.min.x -= Scrollgap; r.min.y = t->fr.r.max.y; r.max.y = t->all.max.y; draw(screen, r, t->fr.cols[BACK], nil, ZP); } return t->all.max.y; } void textclose(Text* t) { free(t->cache); frclear(&t->fr, 1); filedeltext(t->file, t); t->file = nil; rfclose(t->reffont); if (argtext == t) argtext = nil; if (typetext == t) typetext = nil; if (seltext == t) seltext = nil; if (mousetext == t) mousetext = nil; if (barttext == t) barttext = nil; } int dircmp(const void* a, const void* b) { Dirlist *da, *db; int i, n; da = *(Dirlist**)a; db = *(Dirlist**)b; n = min(da->nr, db->nr); i = memcmp(da->r, db->r, n * sizeof(Rune)); if (i) return i; return da->nr - db->nr; } void textcolumnate(Text* t, Dirlist** dlp, int ndl) { int i, j, w, colw, mint, maxt, ncol, nrow; Dirlist* dl; uint q1; static Rune Lnl[] = {'\n', 0}; static Rune Ltab[] = {'\t', 0}; if (t->file->ntext > 1) return; mint = stringwidth(t->fr.font, "0"); /* go for narrower tabs if set more than 3 wide */ t->fr.maxtab = min(maxtab, TABDIR) * mint; maxt = t->fr.maxtab; colw = 0; for (i = 0; i < ndl; i++) { dl = dlp[i]; w = dl->wid; if (maxt - w % maxt < mint || w % maxt == 0) w += mint; if (w % maxt) w += maxt - (w % maxt); if (w > colw) colw = w; } if (colw == 0) ncol = 1; else ncol = max(1, Dx(t->fr.r) / colw); nrow = (ndl + ncol - 1) / ncol; q1 = 0; for (i = 0; i < nrow; i++) { for (j = i; j < ndl; j += nrow) { dl = dlp[j]; fileinsert(t->file, q1, dl->r, dl->nr); q1 += dl->nr; if (j + nrow >= ndl) break; w = dl->wid; if (maxt - w % maxt < mint) { fileinsert(t->file, q1, Ltab, 1); q1++; w += mint; } do { fileinsert(t->file, q1, Ltab, 1); q1++; w += maxt - (w % maxt); } while (w < colw); } fileinsert(t->file, q1, Lnl, 1); q1++; } } int textload(Text* t, uint q0, char* file, int setqid) { Rune* rp; Dirlist *dl, **dlp; int fd, i, j, n, ndl, nulls; uint q, q1; Dir *d, *dbuf; char* tmp; Text* u; DigestState* h; if (t->ncache != 0 || t->file->b.nc || t->w == nil || t != &t->w->body) error("text.load"); if (t->w->isdir && t->file->nname == 0) { warning(nil, "empty directory name"); return -1; } if (ismtpt(file)) { warning(nil, "will not open self mount point %s\n", file); return -1; } fd = open(file, OREAD); if (fd < 0) { warning(nil, "can't open %s: %r\n", file); return -1; } d = dirfstat(fd); if (d == nil) { warning(nil, "can't fstat %s: %r\n", file); goto Rescue; } nulls = FALSE; h = nil; if (d->qid.type & QTDIR) { /* this is checked in get() but it's possible the file changed underfoot */ if (t->file->ntext > 1) { warning( nil, "%s is a directory; can't read with multiple windows on it\n", file); goto Rescue; } t->w->isdir = TRUE; t->w->filemenu = FALSE; if (t->file->nname > 0 && t->file->name[t->file->nname - 1] != '/') { rp = runemalloc(t->file->nname + 1); runemove(rp, t->file->name, t->file->nname); rp[t->file->nname] = '/'; winsetname(t->w, rp, t->file->nname + 1); free(rp); } dlp = nil; ndl = 0; dbuf = nil; while ((n = dirread(fd, &dbuf)) > 0) { for (i = 0; i < n; i++) { dl = emalloc(sizeof(Dirlist)); j = strlen(dbuf[i].name); tmp = emalloc(j + 1 + 1); memmove(tmp, dbuf[i].name, j); if (dbuf[i].qid.type & QTDIR) tmp[j++] = '/'; tmp[j] = '\0'; dl->r = bytetorune(tmp, &dl->nr); dl->wid = stringwidth(t->fr.font, tmp); free(tmp); ndl++; dlp = realloc(dlp, ndl * sizeof(Dirlist*)); dlp[ndl - 1] = dl; } free(dbuf); } qsort(dlp, ndl, sizeof(Dirlist*), dircmp); t->w->dlp = dlp; t->w->ndl = ndl; textcolumnate(t, dlp, ndl); q1 = t->file->b.nc; } else { t->w->isdir = FALSE; t->w->filemenu = TRUE; if (q0 == 0) h = sha1(nil, 0, nil, nil); q1 = q0 + fileload(t->file, q0, fd, &nulls, h); } if (setqid) { if (h != nil) { sha1(nil, 0, t->file->sha1, h); h = nil; } else { memset(t->file->sha1, 0, sizeof t->file->sha1); } t->file->dev = d->dev; t->file->mtime = d->mtime; t->file->qidpath = d->qid.path; } close(fd); rp = fbufalloc(); for (q = q0; q < q1; q += n) { n = q1 - q; if (n > RBUFSIZE) n = RBUFSIZE; bufread(&t->file->b, q, rp, n); if (q < t->org) t->org += n; else if (q <= t->org + t->fr.nchars) frinsert(&t->fr, rp, rp + n, q - t->org); if (t->fr.lastlinefull) break; } fbuffree(rp); for (i = 0; i < t->file->ntext; i++) { u = t->file->text[i]; if (u != t) { if (u->org > u->file->b.nc) /* will be 0 because of reset(), but safety first */ u->org = 0; textresize(u, u->all, TRUE); textbacknl(u, u->org, 0); /* go to beginning of line */ } textsetselect(u, q0, q0); } if (nulls) warning(nil, "%s: NUL bytes elided\n", file); free(d); return q1 - q0; Rescue: close(fd); return -1; } uint textbsinsert(Text* t, uint q0, Rune* r, uint n, int tofile, int* nrp) { Rune *bp, *tp, *up; int i, initial; if (t->what == Tag) { /* can't happen but safety first: mustn't backspace over file name */ Err: textinsert(t, q0, r, n, tofile); *nrp = n; return q0; } bp = r; for (i = 0; i < n; i++) if (*bp++ == '\b') { --bp; initial = 0; tp = runemalloc(n); runemove(tp, r, i); up = tp + i; for (; i < n; i++) { *up = *bp++; if (*up == '\b') if (up == tp) initial++; else --up; else up++; } if (initial) { if (initial > q0) initial = q0; q0 -= initial; textdelete(t, q0, q0 + initial, tofile); } n = up - tp; textinsert(t, q0, tp, n, tofile); free(tp); *nrp = n; return q0; } goto Err; } void textinsert(Text* t, uint q0, Rune* r, uint n, int tofile) { int c, i; Text* u; if (tofile && t->ncache != 0) error("text.insert"); if (n == 0) return; if (tofile) { fileinsert(t->file, q0, r, n); if (t->what == Body) { t->w->dirty = TRUE; t->w->utflastqid = -1; } if (t->file->ntext > 1) for (i = 0; i < t->file->ntext; i++) { u = t->file->text[i]; if (u != t) { u->w->dirty = TRUE; /* always a body */ textinsert(u, q0, r, n, FALSE); textsetselect(u, u->q0, u->q1); textscrdraw(u); } } } if (q0 < t->iq1) t->iq1 += n; if (q0 < t->q1) t->q1 += n; if (q0 < t->q0) t->q0 += n; if (q0 < t->org) t->org += n; else if (q0 <= t->org + t->fr.nchars) frinsert(&t->fr, r, r + n, q0 - t->org); if (t->w) { c = 'i'; if (t->what == Body) c = 'I'; if (n <= EVENTSIZE) winevent(t->w, "%c%d %d 0 %d %.*S\n", c, q0, q0 + n, n, n, r); else winevent(t->w, "%c%d %d 0 0 \n", c, q0, q0 + n, n); } } void typecommit(Text* t) { if (t->w != nil) wincommit(t->w, t); else textcommit(t, TRUE); } void textfill(Text* t) { Rune* rp; int i, n, m, nl; if (t->fr.lastlinefull || t->nofill) return; if (t->ncache > 0) typecommit(t); rp = fbufalloc(); do { n = t->file->b.nc - (t->org + t->fr.nchars); if (n == 0) break; if (n > 2000) /* educated guess at reasonable amount */ n = 2000; bufread(&t->file->b, t->org + t->fr.nchars, rp, n); /* * it's expensive to frinsert more than we need, so * count newlines. */ nl = t->fr.maxlines - t->fr.nlines; m = 0; for (i = 0; i < n;) { if (rp[i++] == '\n') { m++; if (m >= nl) break; } } frinsert(&t->fr, rp, rp + i, t->fr.nchars); } while (t->fr.lastlinefull == FALSE); fbuffree(rp); } void textdelete(Text* t, uint q0, uint q1, int tofile) { uint n, p0, p1; int i, c; Text* u; if (tofile && t->ncache != 0) error("text.delete"); n = q1 - q0; if (n == 0) return; if (tofile) { filedelete(t->file, q0, q1); if (t->what == Body) { t->w->dirty = TRUE; t->w->utflastqid = -1; } if (t->file->ntext > 1) for (i = 0; i < t->file->ntext; i++) { u = t->file->text[i]; if (u != t) { u->w->dirty = TRUE; /* always a body */ textdelete(u, q0, q1, FALSE); textsetselect(u, u->q0, u->q1); textscrdraw(u); } } } if (q0 < t->iq1) t->iq1 -= min(n, t->iq1 - q0); if (q0 < t->q0) t->q0 -= min(n, t->q0 - q0); if (q0 < t->q1) t->q1 -= min(n, t->q1 - q0); if (q1 <= t->org) t->org -= n; else if (q0 < t->org + t->fr.nchars) { p1 = q1 - t->org; if (p1 > t->fr.nchars) p1 = t->fr.nchars; if (q0 < t->org) { t->org = q0; p0 = 0; } else p0 = q0 - t->org; frdelete(&t->fr, p0, p1); textfill(t); } if (t->w) { c = 'd'; if (t->what == Body) c = 'D'; winevent(t->w, "%c%d %d 0 0 \n", c, q0, q1); } } void textconstrain(Text* t, uint q0, uint q1, uint* p0, uint* p1) { *p0 = min(q0, t->file->b.nc); *p1 = min(q1, t->file->b.nc); } Rune textreadc(Text* t, uint q) { Rune r; if (t->cq0 <= q && q < t->cq0 + t->ncache) r = t->cache[q - t->cq0]; else bufread(&t->file->b, q, &r, 1); return r; } static int spacesindentbswidth(Text* t) { uint q, col; Rune r; col = textbswidth(t, 0x15); q = t->q0; while (q > 0) { r = textreadc(t, q - 1); if (r != ' ') break; q--; if (--col % t->tabstop == 0) break; } if (t->q0 == q) return 1; return t->q0 - q; } int textbswidth(Text* t, Rune c) { uint q, eq; Rune r; int skipping; /* there is known to be at least one character to erase */ if (c == 0x08) { /* ^H: erase character */ if (t->what == Body && t->w->indent[SPACESINDENT]) return spacesindentbswidth(t); return 1; } q = t->q0; skipping = TRUE; while (q > 0) { r = textreadc(t, q - 1); if (r == '\n') { /* eat at most one more character */ if (q == t->q0) /* eat the newline */ --q; break; } if (c == 0x17) { eq = isalnum(r); if (eq && skipping) /* found one; stop skipping */ skipping = FALSE; else if (!eq && !skipping) break; } --q; } return t->q0 - q; } int textfilewidth(Text* t, uint q0, int oneelement) { uint q; Rune r; q = q0; while (q > 0) { r = textreadc(t, q - 1); if (r <= ' ') break; if (oneelement && r == '/') break; --q; } return q0 - q; } Rune* textcomplete(Text* t) { int i, nstr, npath; uint q; Rune tmp[200]; Rune *str, *path; Rune* rp; Completion* c; char *s, *dirs; Runestr dir; /* control-f: filename completion; works back to white space or / */ if (t->q0 < t->file->b.nc && textreadc(t, t->q0) > ' ') /* must be at end of word */ return nil; nstr = textfilewidth(t, t->q0, TRUE); str = runemalloc(nstr); npath = textfilewidth(t, t->q0 - nstr, FALSE); path = runemalloc(npath); c = nil; rp = nil; dirs = nil; q = t->q0 - nstr; for (i = 0; i < nstr; i++) str[i] = textreadc(t, q++); q = t->q0 - nstr - npath; for (i = 0; i < npath; i++) path[i] = textreadc(t, q++); /* is path rooted? if not, we need to make it relative to window path */ if (npath > 0 && path[0] == '/') dir = runestr(path, npath); else { dir = dirname(t, nil, 0); if (dir.nr + 1 + npath > nelem(tmp)) { free(dir.r); goto Return; } if (dir.nr == 0) { dir.nr = 1; dir.r = runestrdup(Ldot); } runemove(tmp, dir.r, dir.nr); tmp[dir.nr] = '/'; runemove(tmp + dir.nr + 1, path, npath); free(dir.r); dir.r = tmp; dir.nr += 1 + npath; dir = cleanrname(dir); } s = smprint("%.*S", nstr, str); dirs = smprint("%.*S", dir.nr, dir.r); c = complete(dirs, s); free(s); if (c == nil) { warning(nil, "error attempting completion: %r\n"); goto Return; } if (!c->advance) { warning( nil, "%.*S%s%.*S*%s\n", dir.nr, dir.r, dir.nr > 0 && dir.r[dir.nr - 1] != '/' ? "/" : "", nstr, str, c->nmatch ? "" : ": no matches in:"); for (i = 0; i < c->nfile; i++) warning(nil, " %s\n", c->filename[i]); } if (c->advance) rp = runesmprint("%s", c->string); else rp = nil; Return: freecompletion(c); free(dirs); free(str); free(path); return rp; } void texttype(Text* t, Rune r) { uint q0, q1; int nnb, nb, n, i; int nr; Rune* rp; Text* u; if (t->what != Body && t->what != Tag && r == '\n') return; if (t->what == Tag) t->w->tagsafe = FALSE; nr = 1; rp = &r; switch (r) { case Kleft: typecommit(t); if (t->q0 > 0) textshow(t, t->q0 - 1, t->q0 - 1, TRUE); return; case Kright: typecommit(t); if (t->q1 < t->file->b.nc) textshow(t, t->q1 + 1, t->q1 + 1, TRUE); return; case Kdown: typecommit(t); q0 = t->q0; nnb = 0; if (t->q0 > 0 && textreadc(t, t->q0 - 1) != '\n') nnb = textbswidth(t, 0x15); while (q0 < t->file->b.nc && textreadc(t, q0) != '\n') q0++; if (q0 == t->file->b.nc) { textshow(t, q0, q0, TRUE); return; } q0++; while (nnb >= 0 && q0 < t->file->b.nc) { if (textreadc(t, q0) == '\n') break; nnb--; if (nnb >= 0) q0++; } textshow(t, q0, q0, TRUE); return; case Kscrollonedown: if (t->what == Tag) goto Tagdown; n = mousescrollsize(t->fr.maxlines); if (n <= 0) n = 1; goto case_Down; case Kpgdown: n = 2 * t->fr.maxlines / 3; case_Down: q0 = t->org + frcharofpt( &t->fr, Pt(t->fr.r.min.x, t->fr.r.min.y + n * t->fr.font->height)); textsetorigin(t, q0, TRUE); return; case Kup: typecommit(t); nnb = 0; if (t->q0 > 0 && textreadc(t, t->q0 - 1) != '\n') nnb = textbswidth(t, 0x15); q1 = nnb; if (t->q0 - nnb > 1 && textreadc(t, t->q0 - nnb - 1) == '\n') nnb++; q0 = t->q0 - nnb; textshow(t, q0, q0, TRUE); nnb = textbswidth(t, 0x15); if (nnb <= 1) return; q0 = q0 - nnb; while (q1 > 0 && textreadc(t, q0) != '\n') { q1--; q0++; } textshow(t, q0, q0, TRUE); return; case Kscrolloneup: if (t->what == Tag) goto Tagup; n = mousescrollsize(t->fr.maxlines); goto case_Up; case Kpgup: n = 2 * t->fr.maxlines / 3; case_Up: q0 = textbacknl(t, t->org, n); textsetorigin(t, q0, TRUE); return; case Khome: case 0x01: /* ^A: beginning of line */ typecommit(t); /* go to where ^U would erase, if not already at BOL */ nnb = 0; if (t->q0 > 0 && textreadc(t, t->q0 - 1) != '\n') nnb = textbswidth(t, 0x15); textshow(t, t->q0 - nnb, t->q0 - nnb, TRUE); return; case Kend: case 0x05: /* ^E: end of line */ typecommit(t); q0 = t->q0; while (q0 < t->file->b.nc && textreadc(t, q0) != '\n') q0++; textshow(t, q0, q0, TRUE); return; case 0x03: /* Ctrl-c: copy */ case Kcmd + 'c': /* %C: copy */ typecommit(t); cut(t, t, nil, TRUE, FALSE, nil, 0); return; case 0x1a: /* Ctrl-z: undo */ case Kcmd + 'z': /* %Z: undo */ typecommit(t); undo(t, nil, nil, TRUE, 0, nil, 0); return; case 0x19: /* Ctrl-y: redo */ case Kcmd + 'Z': /* %-shift-Z: redo */ typecommit(t); undo(t, nil, nil, FALSE, 0, nil, 0); return; case 0x13: /* Ctrl-S: put file */ case Kcmd + 's': typecommit(t); put(&(t->w)->body, nil, nil, XXX, XXX, nil, 0); return; Tagdown: /* expand tag to show all text */ if (!t->w->tagexpand) { t->w->tagexpand = TRUE; winresize(t->w, t->w->r, FALSE, TRUE); } return; Tagup: /* shrink tag to single line */ if (t->w->tagexpand) { t->w->tagexpand = FALSE; t->w->taglines = 1; winresize(t->w, t->w->r, FALSE, TRUE); } return; } if (t->what == Body) { seq++; filemark(t->file); } /* cut/paste must be done after the seq++/filemark */ switch (r) { case 0x18: /* Ctrl-X: cut */ case Kcmd + 'x': /* %X: cut */ typecommit(t); if (t->what == Body) { seq++; filemark(t->file); } cut(t, t, nil, TRUE, TRUE, nil, 0); textshow(t, t->q0, t->q0, 1); t->iq1 = t->q0; return; case 0x16: /* Ctrl-V: paste */ case Kcmd + 'v': /* %V: paste */ typecommit(t); if (t->what == Body) { seq++; filemark(t->file); } paste(t, t, nil, TRUE, FALSE, nil, 0); textshow(t, t->q0, t->q1, 1); t->iq1 = t->q1; return; } if (t->q1 > t->q0) { if (t->ncache != 0) error("text.type"); cut(t, t, nil, TRUE, TRUE, nil, 0); t->eq0 = ~0; } textshow(t, t->q0, t->q0, 1); switch (r) { case 0x06: /* ^F: complete */ case Kins: typecommit(t); rp = textcomplete(t); if (rp == nil) return; nr = runestrlen(rp); break; /* fall through to normal insertion case */ case 0x1B: if (t->eq0 != ~0) { if (t->eq0 <= t->q0) textsetselect(t, t->eq0, t->q0); else textsetselect(t, t->q0, t->eq0); } if (t->ncache > 0) typecommit(t); t->iq1 = t->q0; return; case 0x08: /* ^H: erase character */ case 0x15: /* ^U: erase line */ case 0x17: /* ^W: erase word */ if (t->q0 == 0) /* nothing to erase */ return; nnb = textbswidth(t, r); q1 = t->q0; q0 = q1 - nnb; /* if selection is at beginning of window, avoid deleting invisible text */ if (q0 < t->org) { q0 = t->org; nnb = q1 - q0; } if (nnb <= 0) return; for (i = 0; i < t->file->ntext; i++) { u = t->file->text[i]; u->nofill = TRUE; nb = nnb; n = u->ncache; if (n > 0) { if (q1 != u->cq0 + n) error("text.type backspace"); if (n > nb) n = nb; u->ncache -= n; textdelete(u, q1 - n, q1, FALSE); nb -= n; } if (u->eq0 == q1 || u->eq0 == ~0) u->eq0 = q0; if (nb && u == t) textdelete(u, q0, q0 + nb, TRUE); if (u != t) textsetselect(u, u->q0, u->q1); else textsetselect(t, q0, q0); u->nofill = FALSE; } for (i = 0; i < t->file->ntext; i++) textfill(t->file->text[i]); t->iq1 = t->q0; return; case '\t': if (t->what == Body && t->w->indent[SPACESINDENT]) { nnb = textbswidth(t, 0x15); if (nnb == 1 && textreadc(t, t->q0 - 1) == '\n') nnb = 0; nnb = t->tabstop - nnb % t->tabstop; rp = runemalloc(nnb); for (nr = 0; nr < nnb; nr++) rp[nr] = ' '; } break; case '\n': if (t->what == Body && t->w->indent[AUTOINDENT]) { /* find beginning of previous line using backspace code */ nnb = textbswidth(t, 0x15); /* ^U case */ rp = runemalloc(nnb + 1); nr = 0; rp[nr++] = r; for (i = 0; i < nnb; i++) { r = textreadc(t, t->q0 - nnb + i); if (r != ' ' && r != '\t') break; rp[nr++] = r; } } break; /* fall through to normal code */ } /* otherwise ordinary character; just insert, typically in caches of all texts */ for (i = 0; i < t->file->ntext; i++) { u = t->file->text[i]; if (u->eq0 == ~0) u->eq0 = t->q0; if (u->ncache == 0) u->cq0 = t->q0; else if (t->q0 != u->cq0 + u->ncache) error("text.type cq1"); /* * Change the tag before we add to ncache, * so that if the window body is resized the * commit will not find anything in ncache. */ if (u->what == Body && u->ncache == 0) { u->needundo = TRUE; winsettag(t->w); u->needundo = FALSE; } textinsert(u, t->q0, rp, nr, FALSE); if (u != t) textsetselect(u, u->q0, u->q1); if (u->ncache + nr > u->ncachealloc) { u->ncachealloc += 10 + nr; u->cache = runerealloc(u->cache, u->ncachealloc); } runemove(u->cache + u->ncache, rp, nr); u->ncache += nr; } if (rp != &r) free(rp); textsetselect(t, t->q0 + nr, t->q0 + nr); if (r == '\n' && t->w != nil) wincommit(t->w, t); t->iq1 = t->q0; } void textcommit(Text* t, int tofile) { if (t->ncache == 0) return; if (tofile) fileinsert(t->file, t->cq0, t->cache, t->ncache); if (t->what == Body) { t->w->dirty = TRUE; t->w->utflastqid = -1; } t->ncache = 0; } static Text* clicktext; static uint clickmsec; static Text* selecttext; static uint selectq; /* * called from frame library */ void framescroll(Frame* f, int dl) { if (f != &selecttext->fr) error("frameselect not right frame"); textframescroll(selecttext, dl); } void textframescroll(Text* t, int dl) { uint q0; if (dl == 0) { scrsleep(100); return; } if (dl < 0) { q0 = textbacknl(t, t->org, -dl); if (selectq > t->org + t->fr.p0) textsetselect(t, t->org + t->fr.p0, selectq); else textsetselect(t, selectq, t->org + t->fr.p0); } else { if (t->org + t->fr.nchars == t->file->b.nc) return; q0 = t->org + frcharofpt( &t->fr, Pt(t->fr.r.min.x, t->fr.r.min.y + dl * t->fr.font->height)); if (selectq > t->org + t->fr.p1) textsetselect(t, t->org + t->fr.p1, selectq); else textsetselect(t, selectq, t->org + t->fr.p1); } textsetorigin(t, q0, TRUE); } void textselect(Text* t) { uint q0, q1; int b, x, y; int state; enum { None, Cut, Paste }; selecttext = t; /* * To have double-clicking and chording, we double-click * immediately if it might make sense. */ b = mouse->buttons; q0 = t->q0; q1 = t->q1; selectq = t->org + frcharofpt(&t->fr, mouse->xy); if (clicktext == t && mouse->msec - clickmsec < 500) if (q0 == q1 && selectq == q0) { textdoubleclick(t, &q0, &q1); textsetselect(t, q0, q1); flushimage(display, 1); x = mouse->xy.x; y = mouse->xy.y; /* stay here until something interesting happens */ do readmouse(mousectl); while (mouse->buttons == b && abs(mouse->xy.x - x) < 3 && abs(mouse->xy.y - y) < 3); mouse->xy.x = x; /* in case we're calling frselect */ mouse->xy.y = y; q0 = t->q0; /* may have changed */ q1 = t->q1; selectq = q0; } if (mouse->buttons == b) { t->fr.scroll = framescroll; frselect(&t->fr, mousectl); /* horrible botch: while asleep, may have lost selection altogether */ if (selectq > t->file->b.nc) selectq = t->org + t->fr.p0; t->fr.scroll = nil; if (selectq < t->org) q0 = selectq; else q0 = t->org + t->fr.p0; if (selectq > t->org + t->fr.nchars) q1 = selectq; else q1 = t->org + t->fr.p1; } if (q0 == q1) { if (q0 == t->q0 && clicktext == t && mouse->msec - clickmsec < 500) { textdoubleclick(t, &q0, &q1); clicktext = nil; } else { clicktext = t; clickmsec = mouse->msec; } } else clicktext = nil; textsetselect(t, q0, q1); flushimage(display, 1); state = None; /* what we've done; undo when possible */ while (mouse->buttons) { mouse->msec = 0; b = mouse->buttons; if ((b & 1) && (b & 6)) { if (state == None && t->what == Body) { seq++; filemark(t->w->body.file); } if (b & 2) { if (state == Paste && t->what == Body) { winundo(t->w, TRUE); textsetselect(t, q0, t->q1); state = None; } else if (state != Cut) { cut(t, t, nil, TRUE, TRUE, nil, 0); state = Cut; } } else { if (state == Cut && t->what == Body) { winundo(t->w, TRUE); textsetselect(t, q0, t->q1); state = None; } else if (state != Paste) { paste(t, t, nil, TRUE, FALSE, nil, 0); state = Paste; } } textscrdraw(t); clearmouse(); } flushimage(display, 1); while (mouse->buttons == b) readmouse(mousectl); clicktext = nil; } } void textshow(Text* t, uint q0, uint q1, int doselect) { int qe; int nl; int tsd; int nc; uint q; if (t->what != Body) { if (doselect) textsetselect(t, q0, q1); return; } if (t->w != nil && t->fr.maxlines == 0) colgrow(t->col, t->w, 1); if (doselect) textsetselect(t, q0, q1); qe = t->org + t->fr.nchars; tsd = FALSE; /* do we call textscrdraw? */ nc = t->file->b.nc + t->ncache; if (t->org <= q0) { if (nc == 0 || q0 < qe) tsd = TRUE; else if (q0 == qe && qe == nc) { if (textreadc(t, nc - 1) == '\n') { if (t->fr.nlines < t->fr.maxlines) tsd = TRUE; } else tsd = TRUE; } } if (tsd) textscrdraw(t); else { if (t->w->nopen[QWevent] > 0) nl = 3 * t->fr.maxlines / 4; else nl = t->fr.maxlines / 4; q = textbacknl(t, q0, nl); /* avoid going backwards if trying to go forwards - long lines! */ if (!(q0 > t->org && q < t->org)) textsetorigin(t, q, TRUE); while (q0 > t->org + t->fr.nchars) textsetorigin(t, t->org + 1, FALSE); } } static int region(int a, int b) { if (a < b) return -1; if (a == b) return 0; return 1; } void selrestore(Frame* f, Point pt0, uint p0, uint p1) { if (p1 <= f->p0 || p0 >= f->p1) { /* no overlap */ frdrawsel0(f, pt0, p0, p1, f->cols[BACK], f->cols[TEXT]); return; } if (p0 >= f->p0 && p1 <= f->p1) { /* entirely inside */ frdrawsel0(f, pt0, p0, p1, f->cols[HIGH], f->cols[HTEXT]); return; } /* they now are known to overlap */ /* before selection */ if (p0 < f->p0) { frdrawsel0(f, pt0, p0, f->p0, f->cols[BACK], f->cols[TEXT]); p0 = f->p0; pt0 = frptofchar(f, p0); } /* after selection */ if (p1 > f->p1) { frdrawsel0( f, frptofchar(f, f->p1), f->p1, p1, f->cols[BACK], f->cols[TEXT]); p1 = f->p1; } /* inside selection */ frdrawsel0(f, pt0, p0, p1, f->cols[HIGH], f->cols[HTEXT]); } void textsetselect(Text* t, uint q0, uint q1) { int p0, p1, ticked; /* t->fr.p0 and t->fr.p1 are always right; t->q0 and t->q1 may be off */ t->q0 = q0; t->q1 = q1; /* compute desired p0,p1 from q0,q1 */ p0 = q0 - t->org; p1 = q1 - t->org; ticked = 1; if (p0 < 0) { ticked = 0; p0 = 0; } if (p1 < 0) p1 = 0; if (p0 > t->fr.nchars) p0 = t->fr.nchars; if (p1 > t->fr.nchars) { ticked = 0; p1 = t->fr.nchars; } if (p0 == t->fr.p0 && p1 == t->fr.p1) { if (p0 == p1 && ticked != t->fr.ticked) frtick(&t->fr, frptofchar(&t->fr, p0), ticked); return; } if (p0 > p1) sysfatal( "acme: textsetselect p0=%d p1=%d q0=%ud q1=%ud t->org=%d nchars=%d", p0, p1, q0, q1, (int)t->org, (int)t->fr.nchars); /* screen disagrees with desired selection */ if (t->fr.p1 <= p0 || p1 <= t->fr.p0 || p0 == p1 || t->fr.p1 == t->fr.p0) { /* no overlap or too easy to bother trying */ frdrawsel(&t->fr, frptofchar(&t->fr, t->fr.p0), t->fr.p0, t->fr.p1, 0); if (p0 != p1 || ticked) frdrawsel(&t->fr, frptofchar(&t->fr, p0), p0, p1, 1); goto Return; } /* overlap; avoid unnecessary painting */ if (p0 < t->fr.p0) { /* extend selection backwards */ frdrawsel(&t->fr, frptofchar(&t->fr, p0), p0, t->fr.p0, 1); } else if (p0 > t->fr.p0) { /* trim first part of selection */ frdrawsel(&t->fr, frptofchar(&t->fr, t->fr.p0), t->fr.p0, p0, 0); } if (p1 > t->fr.p1) { /* extend selection forwards */ frdrawsel(&t->fr, frptofchar(&t->fr, t->fr.p1), t->fr.p1, p1, 1); } else if (p1 < t->fr.p1) { /* trim last part of selection */ frdrawsel(&t->fr, frptofchar(&t->fr, p1), p1, t->fr.p1, 0); } Return: t->fr.p0 = p0; t->fr.p1 = p1; } /* * Release the button in less than DELAY ms and it's considered a null selection * if the mouse hardly moved, regardless of whether it crossed a char boundary. */ enum { DELAY = 2, MINMOVE = 4 }; uint xselect( Frame* f, Mousectl* mc, Image* col, uint* p1p) /* when called, button is down */ { uint p0, p1, q, tmp; ulong msec; Point mp, pt0, pt1, qt; int reg, b; mp = mc->m.xy; b = mc->m.buttons; msec = mc->m.msec; /* remove tick */ if (f->p0 == f->p1) frtick(f, frptofchar(f, f->p0), 0); p0 = p1 = frcharofpt(f, mp); pt0 = frptofchar(f, p0); pt1 = frptofchar(f, p1); reg = 0; frtick(f, pt0, 1); do { q = frcharofpt(f, mc->m.xy); if (p1 != q) { if (p0 == p1) frtick(f, pt0, 0); if (reg != region(q, p0)) { /* crossed starting point; reset */ if (reg > 0) selrestore(f, pt0, p0, p1); else if (reg < 0) selrestore(f, pt1, p1, p0); p1 = p0; pt1 = pt0; reg = region(q, p0); if (reg == 0) frdrawsel0(f, pt0, p0, p1, col, display->white); } qt = frptofchar(f, q); if (reg > 0) { if (q > p1) frdrawsel0(f, pt1, p1, q, col, display->white); else if (q < p1) selrestore(f, qt, q, p1); } else if (reg < 0) { if (q > p1) selrestore(f, pt1, p1, q); else frdrawsel0(f, qt, q, p1, col, display->white); } p1 = q; pt1 = qt; } if (p0 == p1) frtick(f, pt0, 1); flushimage(f->display, 1); readmouse(mc); } while (mc->m.buttons == b); if ( mc->m.msec - msec < DELAY && p0 != p1 && abs(mp.x - mc->m.xy.x) < MINMOVE && abs(mp.y - mc->m.xy.y) < MINMOVE) { if (reg > 0) selrestore(f, pt0, p0, p1); else if (reg < 0) selrestore(f, pt1, p1, p0); p1 = p0; } if (p1 < p0) { tmp = p0; p0 = p1; p1 = tmp; } pt0 = frptofchar(f, p0); if (p0 == p1) frtick(f, pt0, 0); selrestore(f, pt0, p0, p1); /* restore tick */ if (f->p0 == f->p1) frtick(f, frptofchar(f, f->p0), 1); flushimage(f->display, 1); *p1p = p1; return p0; } int textselect23(Text* t, uint* q0, uint* q1, Image* high, int mask) { uint p0, p1; int buts; p0 = xselect(&t->fr, mousectl, high, &p1); buts = mousectl->m.buttons; if ((buts & mask) == 0) { *q0 = p0 + t->org; *q1 = p1 + t->org; } while (mousectl->m.buttons) readmouse(mousectl); return buts; } int textselect2(Text* t, uint* q0, uint* q1, Text** tp) { int buts; *tp = nil; buts = textselect23(t, q0, q1, but2col, 4); if (buts & 4) return 0; if (buts & 1) { /* pick up argument */ *tp = argtext; return 1; } return 1; } int textselect3(Text* t, uint* q0, uint* q1) { int h; h = (textselect23(t, q0, q1, but3col, 1 | 2) == 0); return h; } static Rune left1[] = {'{', '[', '(', '<', 0xab, 0}; static Rune right1[] = {'}', ']', ')', '>', 0xbb, 0}; static Rune left2[] = {'\n', 0}; static Rune left3[] = {'\'', '"', '`', 0}; static Rune* left[] = {left1, left2, left3, nil}; static Rune* right[] = {right1, left2, left3, nil}; void textdoubleclick(Text* t, uint* q0, uint* q1) { int c, i; Rune *r, *l, *p; uint q; if (textclickhtmlmatch(t, q0, q1)) return; for (i = 0; left[i] != nil; i++) { q = *q0; l = left[i]; r = right[i]; /* try matching character to left, looking right */ if (q == 0) c = '\n'; else c = textreadc(t, q - 1); p = runestrchr(l, c); if (p != nil) { if (textclickmatch(t, c, r[p - l], 1, &q)) *q1 = q - (c != '\n'); return; } /* try matching character to right, looking left */ if (q == t->file->b.nc) c = '\n'; else c = textreadc(t, q); p = runestrchr(r, c); if (p != nil) { if (textclickmatch(t, c, l[p - r], -1, &q)) { *q1 = *q0 + (*q0 < t->file->b.nc && c == '\n'); *q0 = q; if (c != '\n' || q != 0 || textreadc(t, 0) == '\n') (*q0)++; } return; } } /* try filling out word to right */ while (*q1 < t->file->b.nc && isalnum(textreadc(t, *q1))) (*q1)++; /* try filling out word to left */ while (*q0 > 0 && isalnum(textreadc(t, *q0 - 1))) (*q0)--; } int textclickmatch(Text* t, int cl, int cr, int dir, uint* q) { Rune c; int nest; nest = 1; for (;;) { if (dir > 0) { if (*q == t->file->b.nc) break; c = textreadc(t, *q); (*q)++; } else { if (*q == 0) break; (*q)--; c = textreadc(t, *q); } if (c == cr) { if (--nest == 0) return 1; } else if (c == cl) nest++; } return cl == '\n' && nest == 1; } // Is the text starting at location q an html tag? // Return 1 for , -1 for , 0 for no tag or . // Set *q1, if non-nil, to the location after the tag. static int ishtmlstart(Text* t, uint q, uint* q1) { int c, c1, c2; if (q + 2 > t->file->b.nc) return 0; if (textreadc(t, q++) != '<') return 0; c = textreadc(t, q++); c1 = c; c2 = c; while (c != '>') { if (q >= t->file->b.nc) return 0; c2 = c; c = textreadc(t, q++); } if (q1) *q1 = q; if (c1 == '/') // closing tag return -1; if (c2 == '/' || c2 == '!') // open + close tag or comment return 0; return 1; } // Is the text ending at location q an html tag? // Return 1 for , -1 for , 0 for no tag or . // Set *q0, if non-nil, to the start of the tag. static int ishtmlend(Text* t, uint q, uint* q0) { int c, c1, c2; if (q < 2) return 0; if (textreadc(t, --q) != '>') return 0; c = textreadc(t, --q); c1 = c; c2 = c; while (c != '<') { if (q == 0) return 0; c1 = c; c = textreadc(t, --q); } if (q0) *q0 = q; if (c1 == '/') // closing tag return -1; if (c2 == '/' || c2 == '!') // open + close tag or comment return 0; return 1; } int textclickhtmlmatch(Text* t, uint* q0, uint* q1) { int depth, n; uint q, nq; q = *q0; // after opening tag? scan forward for closing tag if (ishtmlend(t, q, nil) == 1) { depth = 1; while (q < t->file->b.nc) { n = ishtmlstart(t, q, &nq); if (n != 0) { depth += n; if (depth == 0) { *q1 = q; return 1; } q = nq; continue; } q++; } } // before closing tag? scan backward for opening tag if (ishtmlstart(t, q, nil) == -1) { depth = -1; while (q > 0) { n = ishtmlend(t, q, &nq); if (n != 0) { depth += n; if (depth == 0) { *q0 = q; return 1; } q = nq; continue; } q--; } } return 0; } uint textbacknl(Text* t, uint p, uint n) { int i, j; /* look for start of this line if n==0 */ if (n == 0 && p > 0 && textreadc(t, p - 1) != '\n') n = 1; i = n; while (i-- > 0 && p > 0) { --p; /* it's at a newline now; back over it */ if (p == 0) break; /* at 128 chars, call it a line anyway */ for (j = 128; --j > 0 && p > 0; p--) if (textreadc(t, p - 1) == '\n') break; } return p; } void textsetorigin(Text* t, uint org, int exact) { int i, a, fixup; Rune* r; uint n; if (org > 0 && !exact && textreadc(t, org - 1) != '\n') { /* org is an estimate of the char posn; find a newline */ /* don't try harder than 256 chars */ for (i = 0; i < 256 && org < t->file->b.nc; i++) { if (textreadc(t, org) == '\n') { org++; break; } org++; } } a = org - t->org; fixup = 0; if (a >= 0 && a < t->fr.nchars) { frdelete(&t->fr, 0, a); fixup = 1; /* frdelete can leave end of last line in wrong selection mode; it doesn't know what follows */ } else if (a < 0 && -a < t->fr.nchars) { n = t->org - org; r = runemalloc(n); bufread(&t->file->b, org, r, n); frinsert(&t->fr, r, r + n, 0); free(r); } else frdelete(&t->fr, 0, t->fr.nchars); t->org = org; textfill(t); textscrdraw(t); textsetselect(t, t->q0, t->q1); if (fixup && t->fr.p1 > t->fr.p0) frdrawsel( &t->fr, frptofchar(&t->fr, t->fr.p1 - 1), t->fr.p1 - 1, t->fr.p1, 1); } void textreset(Text* t) { t->file->seq = 0; t->eq0 = ~0; /* do t->delete(0, t->nc, TRUE) without building backup stuff */ textsetselect(t, t->org, t->org); frdelete(&t->fr, 0, t->fr.nchars); t->org = 0; t->q0 = 0; t->q1 = 0; filereset(t->file); bufreset(&t->file->b); }