9fans.net/go@v0.0.5/cmd/acme/internal/edit/ecmd.go (about)

     1  // #include <u.h>
     2  // #include <libc.h>
     3  // #include <draw.h>
     4  // #include <thread.h>
     5  // #include <cursor.h>
     6  // #include <mouse.h>
     7  // #include <keyboard.h>
     8  // #include <frame.h>
     9  // #include <fcall.h>
    10  // #include <plumb.h>
    11  // #include <libsec.h>
    12  // #include "dat.h"
    13  // #include "edit.h"
    14  // #include "fns.h"
    15  
    16  package edit
    17  
    18  import (
    19  	"fmt"
    20  	"os"
    21  	"strings"
    22  
    23  	"9fans.net/go/cmd/acme/internal/alog"
    24  	"9fans.net/go/cmd/acme/internal/bufs"
    25  	"9fans.net/go/cmd/acme/internal/file"
    26  	"9fans.net/go/cmd/acme/internal/fileload"
    27  	"9fans.net/go/cmd/acme/internal/regx"
    28  	"9fans.net/go/cmd/acme/internal/runes"
    29  	"9fans.net/go/cmd/acme/internal/ui"
    30  	"9fans.net/go/cmd/acme/internal/util"
    31  	"9fans.net/go/cmd/acme/internal/wind"
    32  )
    33  
    34  var BigLock = func() {}
    35  var BigUnlock = func() {}
    36  
    37  var Glooping int
    38  var nest int
    39  var Enoname = "no file name given"
    40  
    41  var TheAddr Address
    42  var menu *wind.File
    43  
    44  // extern var curtext *Text
    45  var collection []rune
    46  
    47  func clearcollection() {
    48  	collection = nil
    49  }
    50  
    51  func resetxec() {
    52  	nest = 0
    53  	Glooping = nest
    54  	clearcollection()
    55  }
    56  
    57  func mkaddr(a *Address, f *wind.File) {
    58  	a.r.Pos = f.Curtext.Q0
    59  	a.r.End = f.Curtext.Q1
    60  	a.f = f
    61  }
    62  
    63  func cmdexec(t *wind.Text, cp *Cmd) bool {
    64  	var w *wind.Window
    65  	if t == nil {
    66  		w = nil
    67  	} else {
    68  		w = t.W
    69  	}
    70  	if w == nil && (cp.addr == nil || cp.addr.typ != '"') && !strings.ContainsRune("bBnqUXY!", cp.cmdc) && (!(cp.cmdc == 'D') || cp.u.text == nil) {
    71  		editerror("no current window")
    72  	}
    73  	i := cmdlookup(cp.cmdc) // will be -1 for '{'
    74  	var f *wind.File
    75  	if t != nil && t.W != nil {
    76  		t = &t.W.Body
    77  		f = t.File
    78  		f.Curtext = t
    79  	}
    80  	var dot Address
    81  	if i >= 0 && cmdtab[i].defaddr != aNo {
    82  		ap := cp.addr
    83  		if ap == nil && cp.cmdc != '\n' {
    84  			ap = newaddr()
    85  			cp.addr = ap
    86  			ap.typ = '.'
    87  			if cmdtab[i].defaddr == aAll {
    88  				ap.typ = '*'
    89  			}
    90  		} else if ap != nil && ap.typ == '"' && ap.next == nil && cp.cmdc != '\n' {
    91  			ap.next = newaddr()
    92  			ap.next.typ = '.'
    93  			if cmdtab[i].defaddr == aAll {
    94  				ap.next.typ = '*'
    95  			}
    96  		}
    97  		if cp.addr != nil { // may be false for '\n' (only)
    98  			none := Address{}
    99  			if f != nil {
   100  				mkaddr(&dot, f)
   101  				TheAddr = cmdaddress(ap, dot, 0)
   102  			} else { // a "
   103  				TheAddr = cmdaddress(ap, none, 0)
   104  			}
   105  			f = TheAddr.f
   106  			t = f.Curtext
   107  		}
   108  	}
   109  	switch cp.cmdc {
   110  	case '{':
   111  		mkaddr(&dot, f)
   112  		if cp.addr != nil {
   113  			dot = cmdaddress(cp.addr, dot, 0)
   114  		}
   115  		for cp = cp.u.cmd; cp != nil; cp = cp.next {
   116  			if dot.r.End > t.Len() {
   117  				editerror("dot extends past end of buffer during { command")
   118  			}
   119  			t.Q0 = dot.r.Pos
   120  			t.Q1 = dot.r.End
   121  			cmdexec(t, cp)
   122  		}
   123  	default:
   124  		if i < 0 {
   125  			editerror("unknown command %c in cmdexec", cp.cmdc)
   126  		}
   127  		return cmdtab[i].fn(t, cp)
   128  	}
   129  	return true
   130  }
   131  
   132  func Edittext(w *wind.Window, q int, r []rune) error {
   133  	f := w.Body.File
   134  	switch Editing {
   135  	case Inactive:
   136  		return fmt.Errorf("permission denied")
   137  	case Inserting:
   138  		eloginsert(f, q, r)
   139  		return nil
   140  	case Collecting:
   141  		collection = append(collection, r...)
   142  		return nil
   143  	default:
   144  		return fmt.Errorf("unknown state in edittext")
   145  	}
   146  }
   147  
   148  // string is known to be NUL-terminated
   149  func filelist(t *wind.Text, r []rune) []rune {
   150  	if len(r) == 0 {
   151  		return nil
   152  	}
   153  	r = runes.SkipBlank(r)
   154  	if len(r) == 0 || r[0] != '<' {
   155  		return runes.Clone(r)
   156  	}
   157  	// use < command to collect text
   158  	clearcollection()
   159  	runpipe(t, '<', r[1:], Collecting)
   160  	return collection
   161  }
   162  
   163  func a_cmd(t *wind.Text, cp *Cmd) bool {
   164  	return fappend(t.File, cp, TheAddr.r.End)
   165  }
   166  
   167  func b_cmd(t *wind.Text, cp *Cmd) bool {
   168  	f := tofile(cp.u.text)
   169  	if nest == 0 {
   170  		pfilename(f)
   171  	}
   172  	curtext = f.Curtext
   173  	return true
   174  }
   175  
   176  func B_cmd(t *wind.Text, cp *Cmd) bool {
   177  	list := filelist(t, cp.u.text.r)
   178  	if list == nil {
   179  		editerror(Enoname)
   180  	}
   181  	r := runes.SkipBlank(list)
   182  	if len(r) == 0 {
   183  		ui.New(t, t, nil, false, false, r)
   184  	} else {
   185  		for len(r) > 0 {
   186  			s := runes.SkipNonBlank(r)
   187  			ui.New(t, t, nil, false, false, r[:len(r)-len(s)])
   188  			r = runes.SkipBlank(s)
   189  		}
   190  	}
   191  	clearcollection()
   192  	return true
   193  }
   194  
   195  func c_cmd(t *wind.Text, cp *Cmd) bool {
   196  	elogreplace(t.File, TheAddr.r.Pos, TheAddr.r.End, cp.u.text.r)
   197  	t.Q0 = TheAddr.r.Pos
   198  	t.Q1 = TheAddr.r.End
   199  	return true
   200  }
   201  
   202  func d_cmd(t *wind.Text, cp *Cmd) bool {
   203  	if TheAddr.r.End > TheAddr.r.Pos {
   204  		elogdelete(t.File, TheAddr.r.Pos, TheAddr.r.End)
   205  	}
   206  	t.Q0 = TheAddr.r.Pos
   207  	t.Q1 = TheAddr.r.Pos
   208  	return true
   209  }
   210  
   211  func D1(t *wind.Text) {
   212  	if len(t.W.Body.File.Text) > 1 || wind.Winclean(t.W, false) {
   213  		ui.ColcloseAndMouse(t.Col, t.W, true)
   214  	}
   215  }
   216  
   217  func D_cmd(t *wind.Text, cp *Cmd) bool {
   218  	list := filelist(t, cp.u.text.r)
   219  	if list == nil {
   220  		D1(t)
   221  		return true
   222  	}
   223  	dir := wind.Dirname(t, nil)
   224  	r := runes.SkipBlank(list)
   225  	for {
   226  		s := runes.SkipNonBlank(r)
   227  		r = r[:len(r)-len(s)]
   228  		var rs []rune
   229  		// first time through, could be empty string, meaning delete file empty name
   230  		if len(r) == 0 || r[0] == '/' || len(dir) == 0 {
   231  			rs = runes.Clone(r)
   232  		} else {
   233  			n := make([]rune, len(dir)+1+len(r))
   234  			copy(n, dir)
   235  			n[len(dir)] = '/'
   236  			copy(n[len(dir)+1:], r)
   237  			rs = runes.CleanPath(n)
   238  		}
   239  		w := ui.LookFile(rs)
   240  		if w == nil {
   241  			editerror(fmt.Sprintf("no such file %s", string(rs)))
   242  		}
   243  		D1(&w.Body)
   244  		r = runes.SkipBlank(s)
   245  		if len(r) == 0 {
   246  			break
   247  		}
   248  	}
   249  	clearcollection()
   250  	return true
   251  }
   252  
   253  func readloader(f *wind.File) func(pos int, data []rune) int {
   254  	return func(pos int, data []rune) int {
   255  		if len(data) > 0 {
   256  			eloginsert(f, pos, data)
   257  		}
   258  		return 0
   259  	}
   260  }
   261  
   262  func e_cmd(t *wind.Text, cp *Cmd) bool {
   263  	f := t.File
   264  	q0 := TheAddr.r.Pos
   265  	q1 := TheAddr.r.End
   266  	if cp.cmdc == 'e' {
   267  		if !wind.Winclean(t.W, true) {
   268  			editerror("") // winclean generated message already
   269  		}
   270  		q0 = 0
   271  		q1 = f.Len()
   272  	}
   273  	allreplaced := (q0 == 0 && q1 == f.Len())
   274  	name := cmdname(f, cp.u.text, cp.cmdc == 'e')
   275  	if name == nil {
   276  		editerror(Enoname)
   277  	}
   278  	samename := runes.Equal(name, t.File.Name())
   279  	s := string(name)
   280  	fd, err := os.Open(s)
   281  	if err != nil {
   282  		editerror(fmt.Sprintf("can't open %s: %v", s, err))
   283  	}
   284  	defer fd.Close()
   285  	if info, err := fd.Stat(); err == nil && info.IsDir() {
   286  		editerror(fmt.Sprintf("%s is a directory", s))
   287  	}
   288  	elogdelete(f, q0, q1)
   289  	nulls := false
   290  	fileload.Loadfile(fd, q1, &nulls, readloader(f), nil)
   291  	if nulls {
   292  		alog.Printf("%s: NUL bytes elided\n", s)
   293  	} else if allreplaced && samename {
   294  		elogfind(f).editclean = true
   295  	}
   296  	return true
   297  }
   298  
   299  func f_cmd(t *wind.Text, cp *Cmd) bool {
   300  	var str *String
   301  	if cp.u.text == nil {
   302  		str = new(String) // empty
   303  	} else {
   304  		str = cp.u.text
   305  	}
   306  	cmdname(t.File, str, true)
   307  	pfilename(t.File)
   308  	return true
   309  }
   310  
   311  func g_cmd(t *wind.Text, cp *Cmd) bool {
   312  	if t.File != TheAddr.f {
   313  		alog.Printf("internal error: g_cmd f!=addr.f\n")
   314  		return false
   315  	}
   316  	if !regx.Compile(cp.re.r) {
   317  		editerror("bad regexp in g command")
   318  	}
   319  	if regx.Match(t, nil, TheAddr.r.Pos, TheAddr.r.End, &regx.Sel) != (cp.cmdc == 'v') {
   320  		t.Q0 = TheAddr.r.Pos
   321  		t.Q1 = TheAddr.r.End
   322  		return cmdexec(t, cp.u.cmd)
   323  	}
   324  	return true
   325  }
   326  
   327  func i_cmd(t *wind.Text, cp *Cmd) bool {
   328  	return fappend(t.File, cp, TheAddr.r.Pos)
   329  }
   330  
   331  func fbufalloc() []rune {
   332  	return make([]rune, bufs.Len/runes.RuneSize)
   333  }
   334  
   335  func fbuffree(b []rune) {}
   336  
   337  func fcopy(f *wind.File, addr2 Address) {
   338  	buf := bufs.AllocRunes()
   339  	var ni int
   340  	for p := TheAddr.r.Pos; p < TheAddr.r.End; p += ni {
   341  		ni = TheAddr.r.End - p
   342  		if ni > bufs.RuneLen {
   343  			ni = bufs.RuneLen
   344  		}
   345  		f.Read(p, buf[:ni])
   346  		eloginsert(addr2.f, addr2.r.End, buf[:ni])
   347  	}
   348  	bufs.FreeRunes(buf)
   349  }
   350  
   351  func move(f *wind.File, addr2 Address) {
   352  	if TheAddr.f != addr2.f || TheAddr.r.End <= addr2.r.Pos {
   353  		elogdelete(f, TheAddr.r.Pos, TheAddr.r.End)
   354  		fcopy(f, addr2)
   355  	} else if TheAddr.r.Pos >= addr2.r.End {
   356  		fcopy(f, addr2)
   357  		elogdelete(f, TheAddr.r.Pos, TheAddr.r.End)
   358  	} else if TheAddr.r.Pos == addr2.r.Pos && TheAddr.r.End == addr2.r.End { // move to self; no-op
   359  	} else {
   360  		editerror("move overlaps itself")
   361  	}
   362  }
   363  
   364  func m_cmd(t *wind.Text, cp *Cmd) bool {
   365  	var dot Address
   366  	mkaddr(&dot, t.File)
   367  	addr2 := cmdaddress(cp.u.mtaddr, dot, 0)
   368  	if cp.cmdc == 'm' {
   369  		move(t.File, addr2)
   370  	} else {
   371  		fcopy(t.File, addr2)
   372  	}
   373  	return true
   374  }
   375  
   376  func p_cmd(t *wind.Text, cp *Cmd) bool {
   377  	return pdisplay(t.File)
   378  }
   379  
   380  func s_cmd(t *wind.Text, cp *Cmd) bool {
   381  	n := cp.num
   382  	op := -1
   383  	if !regx.Compile(cp.re.r) {
   384  		editerror("bad regexp in s command")
   385  	}
   386  	var rp []regx.Ranges
   387  	delta := 0
   388  	didsub := false
   389  	for p1 := TheAddr.r.Pos; p1 <= TheAddr.r.End && regx.Match(t, nil, p1, TheAddr.r.End, &regx.Sel); {
   390  		if regx.Sel.R[0].Pos == regx.Sel.R[0].End { // empty match?
   391  			if regx.Sel.R[0].Pos == op {
   392  				p1++
   393  				continue
   394  			}
   395  			p1 = regx.Sel.R[0].End + 1
   396  		} else {
   397  			p1 = regx.Sel.R[0].End
   398  		}
   399  		op = regx.Sel.R[0].End
   400  		n--
   401  		if n > 0 {
   402  			continue
   403  		}
   404  		rp = append(rp, regx.Sel)
   405  	}
   406  	rbuf := bufs.AllocRunes()
   407  	buf := allocstring(0)
   408  	var err string
   409  	for m := 0; m < len(rp); m++ {
   410  		buf.r = buf.r[:0]
   411  		regx.Sel = rp[m]
   412  		for i := 0; i < len(cp.u.text.r); i++ {
   413  			c := cp.u.text.r[i]
   414  			if c == '\\' && i < len(cp.u.text.r)-1 {
   415  				i++
   416  				c = cp.u.text.r[i]
   417  				if '1' <= c && c <= '9' {
   418  					j := c - '0'
   419  					if regx.Sel.R[j].End-regx.Sel.R[j].Pos > bufs.RuneLen {
   420  						err = "replacement string too long"
   421  						goto Err
   422  					}
   423  					t.File.Read(regx.Sel.R[j].Pos, rbuf[:regx.Sel.R[j].End-regx.Sel.R[j].Pos])
   424  					for k := 0; k < regx.Sel.R[j].End-regx.Sel.R[j].Pos; k++ {
   425  						Straddc(buf, rbuf[k])
   426  					}
   427  				} else {
   428  					Straddc(buf, c)
   429  				}
   430  			} else if c != '&' {
   431  				Straddc(buf, c)
   432  			} else {
   433  				if regx.Sel.R[0].End-regx.Sel.R[0].Pos > bufs.RuneLen {
   434  					err = "right hand side too long in substitution"
   435  					goto Err
   436  				}
   437  				t.File.Read(regx.Sel.R[0].Pos, rbuf[:regx.Sel.R[0].End-regx.Sel.R[0].Pos])
   438  				for k := 0; k < regx.Sel.R[0].End-regx.Sel.R[0].Pos; k++ {
   439  					Straddc(buf, rbuf[k])
   440  				}
   441  			}
   442  		}
   443  		elogreplace(t.File, regx.Sel.R[0].Pos, regx.Sel.R[0].End, buf.r)
   444  		delta -= regx.Sel.R[0].End - regx.Sel.R[0].Pos
   445  		delta += len(buf.r)
   446  		didsub = true
   447  		if !cp.flag {
   448  			break
   449  		}
   450  	}
   451  	freestring(buf)
   452  	bufs.FreeRunes(rbuf)
   453  	if !didsub && nest == 0 {
   454  		editerror("no substitution")
   455  	}
   456  	t.Q0 = TheAddr.r.Pos
   457  	t.Q1 = TheAddr.r.End
   458  	return true
   459  
   460  Err:
   461  	freestring(buf)
   462  	bufs.FreeRunes(rbuf)
   463  	editerror(err)
   464  	return false
   465  }
   466  
   467  func u_cmd(t *wind.Text, cp *Cmd) bool {
   468  	n := cp.num
   469  	flag := true
   470  	if n < 0 {
   471  		n = -n
   472  		flag = false
   473  	}
   474  	oseq := -1
   475  	for {
   476  		tmp3 := n
   477  		n--
   478  		if !(tmp3 > 0) || !(t.File.Seq() != oseq) {
   479  			break
   480  		}
   481  		oseq = t.File.Seq()
   482  		const XXX = false
   483  		ui.XUndo(t, nil, nil, flag, XXX, nil)
   484  	}
   485  	return true
   486  }
   487  
   488  func w_cmd(t *wind.Text, cp *Cmd) bool {
   489  	f := t.File
   490  	if f.Seq() == file.Seq {
   491  		editerror("can't write file with pending modifications")
   492  	}
   493  	r := cmdname(f, cp.u.text, false)
   494  	if r == nil {
   495  		editerror("no name specified for 'w' command")
   496  	}
   497  	Putfile(f, TheAddr.r.Pos, TheAddr.r.End, r)
   498  	// r is freed by putfile
   499  	return true
   500  }
   501  
   502  var Putfile = func(*wind.File, int, int, []rune) {}
   503  
   504  func x_cmd(t *wind.Text, cp *Cmd) bool {
   505  	if cp.re != nil {
   506  		looper(t.File, cp, cp.cmdc == 'x')
   507  	} else {
   508  		linelooper(t.File, cp)
   509  	}
   510  	return true
   511  }
   512  
   513  func X_cmd(t *wind.Text, cp *Cmd) bool {
   514  
   515  	filelooper(t, cp, cp.cmdc == 'X')
   516  	return true
   517  }
   518  
   519  var Run = func(w *wind.Window, s string, rdir []rune) {}
   520  
   521  func runpipe(t *wind.Text, cmd rune, cr []rune, state int) {
   522  	r := runes.SkipBlank(cr)
   523  	if len(r) == 0 {
   524  		editerror("no command specified for %c", cmd)
   525  	}
   526  	var w *wind.Window
   527  	if state == Inserting {
   528  		w = t.W
   529  		t.Q0 = TheAddr.r.Pos
   530  		t.Q1 = TheAddr.r.End
   531  		if cmd == '<' || cmd == '|' {
   532  			elogdelete(t.File, t.Q0, t.Q1)
   533  		}
   534  	}
   535  	s := make([]rune, len(r)+1)
   536  	s[0] = cmd
   537  	copy(s[1:], r)
   538  	var dir []rune
   539  	dir = nil
   540  	if t != nil {
   541  		dir = wind.Dirname(t, nil)
   542  	}
   543  	if len(dir) == 1 && dir[0] == '.' { // sigh
   544  		dir = nil
   545  	}
   546  	Editing = state
   547  	if t != nil && t.W != nil {
   548  		util.Incref(&t.W.Ref) // run will decref
   549  	}
   550  	Run(w, string(s), dir)
   551  	if t != nil && t.W != nil {
   552  		wind.Winunlock(t.W)
   553  	}
   554  	wind.TheRow.Lk.Unlock()
   555  	BigUnlock()
   556  	<-Cedit
   557  
   558  	/*
   559  	 * The editoutlk exists only so that we can tell when
   560  	 * the editout file has been closed.  It can get closed *after*
   561  	 * the process exits because, since the process cannot be
   562  	 * connected directly to editout (no 9P kernel support),
   563  	 * the process is actually connected to a pipe to another
   564  	 * process (arranged via 9pserve) that reads from the pipe
   565  	 * and then writes the data in the pipe to editout using
   566  	 * 9P transactions.  This process might still have a couple
   567  	 * writes left to copy after the original process has exited.
   568  	 */
   569  	var q *util.QLock
   570  	if w != nil {
   571  		q = &w.Editoutlk
   572  	} else {
   573  		q = &Editoutlk
   574  	}
   575  	q.Lock() // wait for file to close
   576  	q.Unlock()
   577  	wind.TheRow.Lk.Lock()
   578  	BigLock()
   579  	Editing = Inactive
   580  	if t != nil && t.W != nil {
   581  		wind.Winlock(t.W, 'M')
   582  	}
   583  }
   584  
   585  func pipe_cmd(t *wind.Text, cp *Cmd) bool {
   586  	runpipe(t, cp.cmdc, cp.u.text.r, Inserting)
   587  	return true
   588  }
   589  
   590  func Nlcount(t *wind.Text, q0 int, q1 int, pnr *int) int {
   591  	buf := bufs.AllocRunes()
   592  	nbuf := 0
   593  	nl := 0
   594  	i := nl
   595  	start := q0
   596  	for q0 < q1 {
   597  		if i == nbuf {
   598  			nbuf = q1 - q0
   599  			if nbuf > bufs.RuneLen {
   600  				nbuf = bufs.RuneLen
   601  			}
   602  			t.File.Read(q0, buf[:nbuf])
   603  			i = 0
   604  		}
   605  		if buf[i] == '\n' {
   606  			start = q0 + 1
   607  			nl++
   608  		}
   609  		i++
   610  		q0++
   611  	}
   612  	bufs.FreeRunes(buf)
   613  	if pnr != nil {
   614  		*pnr = q0 - start
   615  	}
   616  	return nl
   617  }
   618  
   619  const (
   620  	PosnLine      = 0
   621  	PosnChars     = 1
   622  	PosnLineChars = 2
   623  )
   624  
   625  func printposn(t *wind.Text, mode int) {
   626  	if t != nil && t.File != nil && t.File.Name() != nil {
   627  		alog.Printf("%s:", string(t.File.Name()))
   628  	}
   629  	var l1 int
   630  	var l2 int
   631  	var r1 int
   632  	var r2 int
   633  
   634  	switch mode {
   635  	case PosnChars:
   636  		alog.Printf("#%d", TheAddr.r.Pos)
   637  		if TheAddr.r.End != TheAddr.r.Pos {
   638  			alog.Printf(",#%d", TheAddr.r.End)
   639  		}
   640  		alog.Printf("\n")
   641  		return
   642  
   643  	default:
   644  	case PosnLine:
   645  		l1 = 1 + Nlcount(t, 0, TheAddr.r.Pos, nil)
   646  		l2 = l1 + Nlcount(t, TheAddr.r.Pos, TheAddr.r.End, nil)
   647  		// check if addr ends with '\n'
   648  		if TheAddr.r.End > 0 && TheAddr.r.End > TheAddr.r.Pos && t.RuneAt(TheAddr.r.End-1) == '\n' {
   649  			l2--
   650  		}
   651  		alog.Printf("%d", l1)
   652  		if l2 != l1 {
   653  			alog.Printf(",%d", l2)
   654  		}
   655  		alog.Printf("\n")
   656  		return
   657  
   658  	case PosnLineChars:
   659  		l1 = 1 + Nlcount(t, 0, TheAddr.r.Pos, &r1)
   660  		l2 = l1 + Nlcount(t, TheAddr.r.Pos, TheAddr.r.End, &r2)
   661  		if l2 == l1 {
   662  			r2 += r1
   663  		}
   664  		alog.Printf("%d+#%d", l1, r1)
   665  		if l2 != l1 {
   666  			alog.Printf(",%d+#%d", l2, r2)
   667  		}
   668  		alog.Printf("\n")
   669  		return
   670  	}
   671  }
   672  
   673  func eq_cmd(t *wind.Text, cp *Cmd) bool {
   674  	var mode int
   675  	switch len(cp.u.text.r) {
   676  	case 0:
   677  		mode = PosnLine
   678  	case 1:
   679  		if cp.u.text.r[0] == '#' {
   680  			mode = PosnChars
   681  			break
   682  		}
   683  		if cp.u.text.r[0] == '+' {
   684  			mode = PosnLineChars
   685  			break
   686  		}
   687  		fallthrough
   688  	default:
   689  		editerror("newline expected")
   690  	}
   691  	printposn(t, mode)
   692  	return true
   693  }
   694  
   695  func nl_cmd(t *wind.Text, cp *Cmd) bool {
   696  	f := t.File
   697  	if cp.addr == nil {
   698  		var a Address
   699  		// First put it on newline boundaries
   700  		mkaddr(&a, f)
   701  		TheAddr = lineaddr(0, a, -1)
   702  		a = lineaddr(0, a, 1)
   703  		TheAddr.r.End = a.r.End
   704  		if TheAddr.r.Pos == t.Q0 && TheAddr.r.End == t.Q1 {
   705  			mkaddr(&a, f)
   706  			TheAddr = lineaddr(1, a, 1)
   707  		}
   708  	}
   709  	wind.Textshow(t, TheAddr.r.Pos, TheAddr.r.End, true)
   710  	return true
   711  }
   712  
   713  func fappend(f *wind.File, cp *Cmd, p int) bool {
   714  	if len(cp.u.text.r) > 0 {
   715  		eloginsert(f, p, cp.u.text.r)
   716  	}
   717  	f.Curtext.Q0 = p
   718  	f.Curtext.Q1 = p
   719  	return true
   720  }
   721  
   722  func pdisplay(f *wind.File) bool {
   723  	p1 := TheAddr.r.Pos
   724  	p2 := TheAddr.r.End
   725  	if p2 > f.Len() {
   726  		p2 = f.Len()
   727  	}
   728  	buf := bufs.AllocRunes()
   729  	for p1 < p2 {
   730  		np := p2 - p1
   731  		if np > bufs.RuneLen-1 {
   732  			np = bufs.RuneLen - 1
   733  		}
   734  		f.Read(p1, buf[:np])
   735  		alog.Printf("%s", string(buf[:np]))
   736  		p1 += np
   737  	}
   738  	bufs.FreeRunes(buf)
   739  	f.Curtext.Q0 = TheAddr.r.Pos
   740  	f.Curtext.Q1 = TheAddr.r.End
   741  	return true
   742  }
   743  
   744  func pfilename(f *wind.File) {
   745  	w := f.Curtext.W
   746  	// same check for dirty as in settag, but we know ncache==0
   747  	dirty := !w.IsDir && !w.IsScratch && f.Mod()
   748  	ch := func(s string, b bool) byte {
   749  		if b {
   750  			return s[1]
   751  		}
   752  		return s[0]
   753  	}
   754  	alog.Printf("%c%c%c %s\n", ch(" '", dirty), '+', ch(" .", curtext != nil && curtext.File == f), string(f.Name()))
   755  }
   756  
   757  func loopcmd(f *wind.File, cp *Cmd, rp []runes.Range) {
   758  	for i := 0; i < len(rp); i++ {
   759  		f.Curtext.Q0 = rp[i].Pos
   760  		f.Curtext.Q1 = rp[i].End
   761  		cmdexec(f.Curtext, cp)
   762  	}
   763  }
   764  
   765  func looper(f *wind.File, cp *Cmd, xy bool) {
   766  	r := TheAddr.r
   767  	op := r.Pos
   768  	if xy {
   769  		op = -1
   770  	}
   771  	nest++
   772  	if !regx.Compile(cp.re.r) {
   773  		editerror("bad regexp in %c command", cp.cmdc)
   774  	}
   775  	var rp []runes.Range
   776  	for p := r.Pos; p <= r.End; {
   777  		var tr runes.Range
   778  		if !regx.Match(f.Curtext, nil, p, r.End, &regx.Sel) { // no match, but y should still run
   779  			if xy || op > r.End {
   780  				break
   781  			}
   782  			tr.Pos = op
   783  			tr.End = r.End
   784  			p = r.End + 1 // exit next loop
   785  		} else {
   786  			if regx.Sel.R[0].Pos == regx.Sel.R[0].End { // empty match?
   787  				if regx.Sel.R[0].Pos == op {
   788  					p++
   789  					continue
   790  				}
   791  				p = regx.Sel.R[0].End + 1
   792  			} else {
   793  				p = regx.Sel.R[0].End
   794  			}
   795  			if xy {
   796  				tr = regx.Sel.R[0]
   797  			} else {
   798  				tr.Pos = op
   799  				tr.End = regx.Sel.R[0].Pos
   800  			}
   801  		}
   802  		op = regx.Sel.R[0].End
   803  		rp = append(rp, tr)
   804  	}
   805  	loopcmd(f, cp.u.cmd, rp)
   806  	nest--
   807  }
   808  
   809  func linelooper(f *wind.File, cp *Cmd) {
   810  	nest++
   811  	var rp []runes.Range
   812  	r := TheAddr.r
   813  	var a3 Address
   814  	a3.f = f
   815  	a3.r.End = r.Pos
   816  	a3.r.Pos = a3.r.End
   817  	a := lineaddr(0, a3, 1)
   818  	linesel := a.r
   819  	for p := r.Pos; p < r.End; p = a3.r.End {
   820  		a3.r.Pos = a3.r.End
   821  		if p != r.Pos || linesel.End == p {
   822  			a = lineaddr(1, a3, 1)
   823  			linesel = a.r
   824  		}
   825  		if linesel.Pos >= r.End {
   826  			break
   827  		}
   828  		if linesel.End >= r.End {
   829  			linesel.End = r.End
   830  		}
   831  		if linesel.End > linesel.Pos {
   832  			if linesel.Pos >= a3.r.End && linesel.End > a3.r.End {
   833  				a3.r = linesel
   834  				rp = append(rp, linesel)
   835  				continue
   836  			}
   837  		}
   838  		break
   839  	}
   840  	loopcmd(f, cp.u.cmd, rp)
   841  	nest--
   842  }
   843  
   844  type Looper struct {
   845  	cp *Cmd
   846  	XY bool
   847  	w  []*wind.Window
   848  }
   849  
   850  var loopstruct Looper // only one; X and Y can't nest
   851  
   852  func alllooper(w *wind.Window, v interface{}) {
   853  	lp := v.(*Looper)
   854  	cp := lp.cp
   855  	//	if(w->isscratch || w->isdir)
   856  	//		return;
   857  	t := &w.Body
   858  	// only use this window if it's the current window for the file
   859  	if t.File.Curtext != t {
   860  		return
   861  	}
   862  	//	if(w->nopen[QWevent] > 0)
   863  	//		return;
   864  	// no auto-execute on files without names
   865  	if cp.re == nil && len(t.File.Name()) == 0 {
   866  		return
   867  	}
   868  	if cp.re == nil || filematch(t.File, cp.re) == lp.XY {
   869  		lp.w = append(lp.w, w)
   870  	}
   871  }
   872  
   873  func alllocker(w *wind.Window, v interface{}) {
   874  	if v.(bool) {
   875  		util.Incref(&w.Ref)
   876  	} else {
   877  		wind.Winclose(w)
   878  	}
   879  }
   880  
   881  func filelooper(t *wind.Text, cp *Cmd, XY bool) {
   882  	tmp6 := Glooping
   883  	Glooping++
   884  	if tmp6 != 0 {
   885  		cmd := 'Y'
   886  		if XY {
   887  			cmd = 'X'
   888  		}
   889  		editerror("can't nest %c command", cmd)
   890  	}
   891  	nest++
   892  
   893  	loopstruct.cp = cp
   894  	loopstruct.XY = XY
   895  	loopstruct.w = nil
   896  	wind.All(alllooper, &loopstruct)
   897  	/*
   898  	 * add a ref to all windows to keep safe windows accessed by X
   899  	 * that would not otherwise have a ref to hold them up during
   900  	 * the shenanigans.  note this with globalincref so that any
   901  	 * newly created windows start with an extra reference.
   902  	 */
   903  	wind.All(alllocker, true)
   904  	wind.GlobalIncref = 1
   905  
   906  	/*
   907  	 * Unlock the window running the X command.
   908  	 * We'll need to lock and unlock each target window in turn.
   909  	 */
   910  	if t != nil && t.W != nil {
   911  		wind.Winunlock(t.W)
   912  	}
   913  
   914  	for i := 0; i < len(loopstruct.w); i++ {
   915  		targ := &loopstruct.w[i].Body
   916  		if targ != nil && targ.W != nil {
   917  			wind.Winlock(targ.W, cp.cmdc)
   918  		}
   919  		cmdexec(targ, cp.u.cmd)
   920  		if targ != nil && targ.W != nil {
   921  			wind.Winunlock(targ.W)
   922  		}
   923  	}
   924  
   925  	if t != nil && t.W != nil {
   926  		wind.Winlock(t.W, cp.cmdc)
   927  	}
   928  
   929  	wind.All(alllocker, false)
   930  	wind.GlobalIncref = 0
   931  	loopstruct.w = nil
   932  
   933  	Glooping--
   934  	nest--
   935  }
   936  
   937  func nextmatch(f *wind.File, r *String, p int, sign int) {
   938  	if !regx.Compile(r.r) {
   939  		editerror("bad regexp in command address")
   940  	}
   941  	if sign >= 0 {
   942  		if !regx.Match(f.Curtext, nil, p, 0x7FFFFFFF, &regx.Sel) {
   943  			editerror("no match for regexp")
   944  		}
   945  		if regx.Sel.R[0].Pos == regx.Sel.R[0].End && regx.Sel.R[0].Pos == p {
   946  			p++
   947  			if p > f.Len() {
   948  				p = 0
   949  			}
   950  			if !regx.Match(f.Curtext, nil, p, 0x7FFFFFFF, &regx.Sel) {
   951  				editerror("address")
   952  			}
   953  		}
   954  	} else {
   955  		if !regx.MatchBackward(f.Curtext, p, &regx.Sel) {
   956  			editerror("no match for regexp")
   957  		}
   958  		if regx.Sel.R[0].Pos == regx.Sel.R[0].End && regx.Sel.R[0].End == p {
   959  			p--
   960  			if p < 0 {
   961  				p = f.Len()
   962  			}
   963  			if !regx.MatchBackward(f.Curtext, p, &regx.Sel) {
   964  				editerror("address")
   965  			}
   966  		}
   967  	}
   968  }
   969  
   970  func cmdaddress(ap *Addr, a Address, sign int) Address {
   971  	f := a.f
   972  	for {
   973  		var a2 Address
   974  		var a1 Address
   975  		switch ap.typ {
   976  		case 'l':
   977  			a = lineaddr(ap.num, a, sign)
   978  
   979  		case '#':
   980  			a = charaddr(ap.num, a, sign)
   981  
   982  		case '.':
   983  			mkaddr(&a, f)
   984  
   985  		case '$':
   986  			a.r.End = f.Len()
   987  			a.r.Pos = a.r.End
   988  
   989  		case '\'':
   990  			editerror("can't handle '")
   991  			//			a.r = f->mark;
   992  
   993  		case '?':
   994  			sign = -sign
   995  			if sign == 0 {
   996  				sign = -1
   997  			}
   998  			fallthrough
   999  		// fall through
  1000  		case '/':
  1001  			start := a.r.End
  1002  			if sign < 0 {
  1003  				start = a.r.Pos
  1004  			}
  1005  			nextmatch(f, ap.u.re, start, sign)
  1006  			a.r = regx.Sel.R[0]
  1007  
  1008  		case '"':
  1009  			f = matchfile(ap.u.re)
  1010  			mkaddr(&a, f)
  1011  
  1012  		case '*':
  1013  			a.r.Pos = 0
  1014  			a.r.End = f.Len()
  1015  			return a
  1016  
  1017  		case ',',
  1018  			';':
  1019  			if ap.u.left != nil {
  1020  				a1 = cmdaddress(ap.u.left, a, 0)
  1021  			} else {
  1022  				a1.f = a.f
  1023  				a1.r.End = 0
  1024  				a1.r.Pos = a1.r.End
  1025  			}
  1026  			if ap.typ == ';' {
  1027  				f = a1.f
  1028  				a = a1
  1029  				f.Curtext.Q0 = a1.r.Pos
  1030  				f.Curtext.Q1 = a1.r.End
  1031  			}
  1032  			if ap.next != nil {
  1033  				a2 = cmdaddress(ap.next, a, 0)
  1034  			} else {
  1035  				a2.f = a.f
  1036  				a2.r.End = f.Len()
  1037  				a2.r.Pos = a2.r.End
  1038  			}
  1039  			if a1.f != a2.f {
  1040  				editerror("addresses in different files")
  1041  			}
  1042  			a.f = a1.f
  1043  			a.r.Pos = a1.r.Pos
  1044  			a.r.End = a2.r.End
  1045  			if a.r.End < a.r.Pos {
  1046  				editerror("addresses out of order")
  1047  			}
  1048  			return a
  1049  
  1050  		case '+',
  1051  			'-':
  1052  			sign = 1
  1053  			if ap.typ == '-' {
  1054  				sign = -1
  1055  			}
  1056  			if ap.next == nil || ap.next.typ == '+' || ap.next.typ == '-' {
  1057  				a = lineaddr(1, a, sign)
  1058  			}
  1059  		default:
  1060  			util.Fatal("cmdaddress")
  1061  			return a
  1062  		}
  1063  		ap = ap.next
  1064  		if ap == nil { // assign =
  1065  			break
  1066  		}
  1067  	}
  1068  	return a
  1069  }
  1070  
  1071  type Tofile struct {
  1072  	f *wind.File
  1073  	r *String
  1074  }
  1075  
  1076  func alltofile(w *wind.Window, v interface{}) {
  1077  	tp := v.(*Tofile)
  1078  	if tp.f != nil {
  1079  		return
  1080  	}
  1081  	if w.IsScratch || w.IsDir {
  1082  		return
  1083  	}
  1084  	t := &w.Body
  1085  	// only use this window if it's the current window for the file
  1086  	if t.File.Curtext != t {
  1087  		return
  1088  	}
  1089  	//	if(w->nopen[QWevent] > 0)
  1090  	//		return;
  1091  	if runes.Equal(tp.r.r, t.File.Name()) {
  1092  		tp.f = t.File
  1093  	}
  1094  }
  1095  
  1096  func tofile(r *String) *wind.File {
  1097  	var rr String
  1098  	rr.r = runes.SkipBlank(r.r)
  1099  	var t Tofile
  1100  	t.f = nil
  1101  	t.r = &rr
  1102  	wind.All(alltofile, &t)
  1103  	if t.f == nil {
  1104  		editerror("no such file\"%s\"", string(rr.r))
  1105  	}
  1106  	return t.f
  1107  }
  1108  
  1109  func allmatchfile(w *wind.Window, v interface{}) {
  1110  	tp := v.(*Tofile)
  1111  	if w.IsScratch || w.IsDir {
  1112  		return
  1113  	}
  1114  	t := &w.Body
  1115  	// only use this window if it's the current window for the file
  1116  	if t.File.Curtext != t {
  1117  		return
  1118  	}
  1119  	//	if(w->nopen[QWevent] > 0)
  1120  	//		return;
  1121  	if filematch(w.Body.File, tp.r) {
  1122  		if tp.f != nil {
  1123  			editerror("too many files match \"%s\"", string(tp.r.r))
  1124  		}
  1125  		tp.f = w.Body.File
  1126  	}
  1127  }
  1128  
  1129  func matchfile(r *String) *wind.File {
  1130  	var tf Tofile
  1131  	tf.f = nil
  1132  	tf.r = r
  1133  	wind.All(allmatchfile, &tf)
  1134  
  1135  	if tf.f == nil {
  1136  		editerror("no file matches \"%s\"", string(r.r))
  1137  	}
  1138  	return tf.f
  1139  }
  1140  
  1141  func filematch(f *wind.File, r *String) bool {
  1142  	// compile expr first so if we get an error, we haven't allocated anything
  1143  	if !regx.Compile(r.r) {
  1144  		editerror("bad regexp in file match")
  1145  	}
  1146  	w := f.Curtext.W
  1147  	// same check for dirty as in settag, but we know ncache==0
  1148  	dirty := !w.IsDir && !w.IsScratch && f.Mod()
  1149  	ch := func(s string, b bool) byte {
  1150  		if b {
  1151  			return s[1]
  1152  		}
  1153  		return s[0]
  1154  	}
  1155  	rbuf := []rune(fmt.Sprintf("%c%c%c %s\n", ch(" '", dirty), '+', ch(" .", curtext != nil && curtext.File == f), string(f.Name())))
  1156  	var s regx.Ranges
  1157  	return regx.Match(nil, rbuf, 0, len(rbuf), &s)
  1158  }
  1159  
  1160  func charaddr(l int, addr Address, sign int) Address {
  1161  	if sign == 0 {
  1162  		addr.r.End = l
  1163  		addr.r.Pos = addr.r.End
  1164  	} else if sign < 0 {
  1165  		addr.r.Pos -= l
  1166  		addr.r.End = addr.r.Pos
  1167  	} else if sign > 0 {
  1168  		addr.r.End += l
  1169  		addr.r.Pos = addr.r.End
  1170  	}
  1171  	if addr.r.Pos < 0 || addr.r.End > addr.f.Len() {
  1172  		editerror("address out of range")
  1173  	}
  1174  	return addr
  1175  }
  1176  
  1177  func lineaddr(l int, addr Address, sign int) Address {
  1178  	f := addr.f
  1179  	var a Address
  1180  	a.f = f
  1181  	if sign >= 0 {
  1182  		var p int
  1183  		if l == 0 {
  1184  			if sign == 0 || addr.r.End == 0 {
  1185  				a.r.End = 0
  1186  				a.r.Pos = a.r.End
  1187  				return a
  1188  			}
  1189  			a.r.Pos = addr.r.End
  1190  			p = addr.r.End - 1
  1191  		} else {
  1192  			var n int
  1193  			if sign == 0 || addr.r.End == 0 {
  1194  				p = 0
  1195  				n = 1
  1196  			} else {
  1197  				p = addr.r.End - 1
  1198  				if f.Curtext.RuneAt(p) == '\n' {
  1199  					n = 1
  1200  				}
  1201  				p++
  1202  			}
  1203  			for n < l {
  1204  				if p >= f.Len() {
  1205  					editerror("address out of range")
  1206  				}
  1207  				tmp9 := p
  1208  				p++
  1209  				if f.Curtext.RuneAt(tmp9) == '\n' {
  1210  					n++
  1211  				}
  1212  			}
  1213  			a.r.Pos = p
  1214  		}
  1215  		for p < f.Len() {
  1216  			c := f.Curtext.RuneAt(p)
  1217  			p++
  1218  			if c == '\n' {
  1219  				break
  1220  			}
  1221  		}
  1222  		a.r.End = p
  1223  	} else {
  1224  		p := addr.r.Pos
  1225  		if l == 0 {
  1226  			a.r.End = addr.r.Pos
  1227  		} else {
  1228  			for n := 0; n < l; { // always runs once
  1229  				if p == 0 {
  1230  					n++
  1231  					if n != l {
  1232  						editerror("address out of range")
  1233  					}
  1234  				} else {
  1235  					c := f.Curtext.RuneAt(p - 1)
  1236  					if c != '\n' || func() bool { n++; return n != l }() {
  1237  						p--
  1238  					}
  1239  				}
  1240  			}
  1241  			a.r.End = p
  1242  			if p > 0 {
  1243  				p--
  1244  			}
  1245  		}
  1246  		for p > 0 && f.Curtext.RuneAt(p-1) != '\n' { // lines start after a newline
  1247  			p--
  1248  		}
  1249  		a.r.Pos = p
  1250  	}
  1251  	return a
  1252  }
  1253  
  1254  type Filecheck struct {
  1255  	f *wind.File
  1256  	r []rune
  1257  }
  1258  
  1259  func allfilecheck(w *wind.Window, v interface{}) {
  1260  	fp := v.(*Filecheck)
  1261  	f := w.Body.File
  1262  	if w.Body.File == fp.f {
  1263  		return
  1264  	}
  1265  	if runes.Equal(fp.r, f.Name()) {
  1266  		alog.Printf("warning: duplicate file name \"%s\"\n", string(fp.r))
  1267  	}
  1268  }
  1269  
  1270  func cmdname(f *wind.File, str *String, set bool) []rune {
  1271  	s := str.r
  1272  	if len(s) == 0 {
  1273  		// no name; use existing
  1274  		if len(f.Name()) == 0 {
  1275  			return nil
  1276  		}
  1277  		return runes.Clone(f.Name())
  1278  	}
  1279  	s = runes.SkipBlank(s)
  1280  	var r []rune
  1281  	if len(s) > 0 {
  1282  		if s[0] == '/' {
  1283  			r = runes.Clone(s)
  1284  		} else {
  1285  			newname := wind.Dirname(f.Curtext, runes.Clone(s))
  1286  			r = newname
  1287  		}
  1288  		var fc Filecheck
  1289  		fc.f = f
  1290  		fc.r = r
  1291  		wind.All(allfilecheck, &fc)
  1292  		if len(f.Name()) == 0 {
  1293  			set = true
  1294  		}
  1295  	}
  1296  
  1297  	if set && !runes.Equal(r, f.Name()) {
  1298  		f.Mark()
  1299  		f.SetMod(true)
  1300  		f.Curtext.W.Dirty = true
  1301  		wind.Winsetname(f.Curtext.W, r)
  1302  	}
  1303  	return r
  1304  }
  1305  
  1306  // editing
  1307  
  1308  const (
  1309  	Inactive = 0 + iota
  1310  	Inserting
  1311  	Collecting
  1312  )
  1313  
  1314  var Cedit = make(chan int)
  1315  
  1316  var Editoutlk util.QLock // atomic flag