9fans.net/go@v0.0.7/cmd/acme/internal/ui/look.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 <regexp.h>
    11  // #include <9pclient.h>
    12  // #include <plumb.h>
    13  // #include <libsec.h>
    14  // #include "dat.h"
    15  // #include "fns.h"
    16  
    17  package ui
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"path"
    23  
    24  	"9fans.net/go/cmd/acme/internal/addr"
    25  	"9fans.net/go/cmd/acme/internal/adraw"
    26  	"9fans.net/go/cmd/acme/internal/alog"
    27  	"9fans.net/go/cmd/acme/internal/bufs"
    28  	"9fans.net/go/cmd/acme/internal/runes"
    29  	"9fans.net/go/cmd/acme/internal/util"
    30  	"9fans.net/go/cmd/acme/internal/wind"
    31  	"9fans.net/go/draw"
    32  	"9fans.net/go/plan9/client"
    33  	"9fans.net/go/plumb"
    34  )
    35  
    36  func Look3(t *wind.Text, q0, q1 int, external bool) {
    37  	ct := wind.Seltext
    38  	if ct == nil {
    39  		wind.Seltext = t
    40  	}
    41  	var e Expand
    42  	expanded := Expand_(t, q0, q1, &e)
    43  	var n int
    44  	var c rune
    45  	var r []rune
    46  	if !external && t.W != nil && t.W.External {
    47  		// send alphanumeric expansion to external client
    48  		if !expanded {
    49  			return
    50  		}
    51  		f := 0
    52  		if (e.Arg != nil && t.W != nil) || (len(e.Name) > 0 && LookFile(e.Name) != nil) {
    53  			f = 1 // acme can do it without loading a file
    54  		}
    55  		if q0 != e.Q0 || q1 != e.Q1 {
    56  			f |= 2 // second (post-expand) message follows
    57  		}
    58  		if len(e.Name) != 0 {
    59  			f |= 4 // it's a file name
    60  		}
    61  		c = 'l'
    62  		if t.What == wind.Body {
    63  			c = 'L'
    64  		}
    65  		n = q1 - q0
    66  		if n <= wind.EVENTSIZE {
    67  			r := make([]rune, n)
    68  			t.File.Read(q0, r)
    69  			wind.Winevent(t.W, "%c%d %d %d %d %s\n", c, q0, q1, f, n, string(r))
    70  		} else {
    71  			wind.Winevent(t.W, "%c%d %d %d 0 \n", c, q0, q1, f)
    72  		}
    73  		if q0 == e.Q0 && q1 == e.Q1 {
    74  			return
    75  		}
    76  		if len(e.Name) != 0 {
    77  			n = len(e.Name)
    78  			if e.A1 > e.A0 {
    79  				n += 1 + (e.A1 - e.A0)
    80  			}
    81  			r = make([]rune, n)
    82  			copy(r, e.Name)
    83  			if e.A1 > e.A0 {
    84  				r[len(e.Name)] = ':'
    85  				at := e.Arg.(*wind.Text)
    86  				at.File.Read(e.A0, r[len(e.Name)+1:])
    87  			}
    88  		} else {
    89  			n = e.Q1 - e.Q0
    90  			r = make([]rune, n)
    91  			t.File.Read(e.Q0, r)
    92  		}
    93  		f &^= 2
    94  		if n <= wind.EVENTSIZE {
    95  			r := r
    96  			if len(r) > n {
    97  				r = r[:n]
    98  			}
    99  			wind.Winevent(t.W, "%c%d %d %d %d %s\n", c, e.Q0, e.Q1, f, n, string(r))
   100  		} else {
   101  			wind.Winevent(t.W, "%c%d %d %d 0 \n", c, e.Q0, e.Q1, f)
   102  		}
   103  		return
   104  	}
   105  	if Plumbsendfid != nil {
   106  		// send whitespace-delimited word to plumber
   107  		m := new(plumb.Message)
   108  		m.Src = "acme"
   109  		dir := wind.Dirname(t, nil)
   110  		if len(dir) == 1 && dir[0] == '.' { // sigh
   111  			dir = nil
   112  		}
   113  		if len(dir) == 0 {
   114  			m.Dir = Wdir
   115  		} else {
   116  			m.Dir = string(dir)
   117  		}
   118  		m.Type = "text"
   119  		if q1 == q0 {
   120  			if t.Q1 > t.Q0 && t.Q0 <= q0 && q0 <= t.Q1 {
   121  				q0 = t.Q0
   122  				q1 = t.Q1
   123  			} else {
   124  				p := q0
   125  				for q0 > 0 && func() bool { c = tgetc(t, q0-1); return c != ' ' }() && c != '\t' && c != '\n' {
   126  					q0--
   127  				}
   128  				for q1 < t.Len() && func() bool { c = tgetc(t, q1); return c != ' ' }() && c != '\t' && c != '\n' {
   129  					q1++
   130  				}
   131  				if q1 == q0 {
   132  					return
   133  				}
   134  				m.Attr = &plumb.Attribute{Name: "click", Value: fmt.Sprint(p - q0)}
   135  			}
   136  		}
   137  		r = make([]rune, q1-q0)
   138  		t.File.Read(q0, r)
   139  		m.Data = []byte(string(r))
   140  		if len(m.Data) < 7*1024 && m.Send(Plumbsendfid) == nil {
   141  			return
   142  		}
   143  		// plumber failed to match; fall through
   144  	}
   145  
   146  	// interpret alphanumeric string ourselves
   147  	if !expanded {
   148  		return
   149  	}
   150  	if e.Name != nil || e.Arg != nil {
   151  		Openfile(t, &e)
   152  	} else {
   153  		if t.W == nil {
   154  			return
   155  		}
   156  		ct = &t.W.Body
   157  		if t.W != ct.W {
   158  			wind.Winlock(ct.W, 'M')
   159  		}
   160  		if t == ct {
   161  			wind.Textsetselect(ct, e.Q1, e.Q1)
   162  		}
   163  		r = make([]rune, e.Q1-e.Q0)
   164  		t.File.Read(e.Q0, r)
   165  		if Search(ct, r) && e.Jump {
   166  			adraw.Display.MoveCursor(ct.Fr.PointOf(ct.Fr.P0).Add(draw.Pt(4, ct.Fr.Font.Height-4)))
   167  		}
   168  		if t.W != ct.W {
   169  			wind.Winunlock(ct.W)
   170  		}
   171  	}
   172  }
   173  
   174  func Search(ct *wind.Text, r []rune) bool {
   175  	if len(r) == 0 || len(r) > ct.Len() {
   176  		return false
   177  	}
   178  	if 2*len(r) > bufs.RuneLen {
   179  		alog.Printf("string too long\n") // TODO(rsc): why???????
   180  		return false
   181  	}
   182  	maxn := util.Max(2*len(r), bufs.RuneLen)
   183  	s := bufs.AllocRunes()
   184  	b := s[:0]
   185  	around := 0
   186  	q := ct.Q1
   187  	for {
   188  		if q >= ct.Len() {
   189  			q = 0
   190  			around = 1
   191  			b = b[:0]
   192  		}
   193  		if len(b) > 0 {
   194  			i := runes.IndexRune(b, r[0])
   195  			if i < 0 {
   196  				q += len(b)
   197  				b = b[:0]
   198  				if around != 0 && q >= ct.Q1 {
   199  					break
   200  				}
   201  				continue
   202  			}
   203  			q += i
   204  			b = b[i:]
   205  		}
   206  		// reload if buffer covers neither string nor rest of file
   207  		if len(b) < len(r) && len(b) != ct.Len()-q {
   208  			nb := ct.Len() - q
   209  			if nb >= maxn {
   210  				nb = maxn - 1
   211  			}
   212  			ct.File.Read(q, s[:nb])
   213  			b = s[:nb]
   214  		}
   215  		// this runeeq is fishy but the null at b[nb] makes it safe // TODO(rsc): NUL done gone
   216  		if len(b) >= len(r) && runes.Equal(b[:len(r)], r) {
   217  			if ct.W != nil {
   218  				wind.Textshow(ct, q, q+len(r), true)
   219  				wind.Winsettag(ct.W)
   220  			} else {
   221  				ct.Q0 = q
   222  				ct.Q1 = q + len(r)
   223  			}
   224  			wind.Seltext = ct
   225  			bufs.FreeRunes(s)
   226  			return true
   227  		}
   228  		b = b[1:]
   229  		q++
   230  		if around != 0 && q >= ct.Q1 {
   231  			break
   232  		}
   233  	}
   234  	bufs.FreeRunes(s)
   235  	return false
   236  }
   237  
   238  // Runestr wrapper for cleanname
   239  
   240  var includefile_Lslash = [2]rune{'/', 0}
   241  
   242  func includefile(dir []rune, file []rune) []rune {
   243  	a := fmt.Sprintf("%s/%s", string(dir), string(file))
   244  	if _, err := os.Stat(a); err != nil {
   245  		return nil
   246  	}
   247  	return []rune(path.Clean(a))
   248  }
   249  
   250  var objdir []rune
   251  
   252  func includename(t *wind.Text, r []rune) []rune {
   253  	var i int
   254  	if objdir == nil && Objtype != "" {
   255  		buf := fmt.Sprintf("/%s/include", Objtype)
   256  		objdir = []rune(buf)
   257  	}
   258  
   259  	w := t.W
   260  	if len(r) == 0 || r[0] == '/' || w == nil {
   261  		return r
   262  	}
   263  	if len(r) > 2 && r[0] == '.' && r[1] == '/' {
   264  		return r
   265  	}
   266  	var file []rune
   267  	file = nil
   268  	for i = 0; i < len(w.Incl) && file == nil; i++ {
   269  		file = includefile(w.Incl[i], r)
   270  	}
   271  
   272  	if file == nil {
   273  		file = includefile([]rune("/sys/include"), r)
   274  	}
   275  	if file == nil {
   276  		file = includefile([]rune("/usr/local/plan9/include"), r)
   277  	}
   278  	if file == nil {
   279  		file = includefile([]rune("/usr/local/include"), r)
   280  	}
   281  	if file == nil {
   282  		file = includefile([]rune("/usr/include"), r)
   283  	}
   284  	if file == nil && objdir != nil {
   285  		file = includefile(objdir, r)
   286  	}
   287  	if file == nil {
   288  		return r
   289  	}
   290  	return file
   291  }
   292  
   293  func texthas(t *wind.Text, q0 int, r []rune) bool {
   294  	if int(q0) < 0 {
   295  		return false
   296  	}
   297  	for i := 0; i < len(r); i++ {
   298  		if q0+i >= t.Len() || t.RuneAt(q0+i) != r[i] {
   299  			return false
   300  		}
   301  	}
   302  	return true
   303  }
   304  
   305  func hasPrefix(r []rune, s []rune) bool {
   306  	if len(r) < len(s) {
   307  		return false
   308  	}
   309  	for i := 0; i < len(s); i++ {
   310  		if r[i] != s[i] {
   311  			return false
   312  		}
   313  	}
   314  	return true
   315  }
   316  
   317  func expandfile(t *wind.Text, q0 int, q1 int, e *Expand) bool {
   318  	amax := q1
   319  	var c rune
   320  	if q1 == q0 {
   321  		colon := -1
   322  		for q1 < t.Len() {
   323  			c = t.RuneAt(q1)
   324  			if !runes.IsFilename(c) {
   325  				break
   326  			}
   327  			if c == ':' && !texthas(t, q1-4, []rune("http://")) && !texthas(t, q1-5, []rune("https://")) {
   328  				colon = q1
   329  				break
   330  			}
   331  			q1++
   332  		}
   333  		for q0 > 0 {
   334  			c = t.RuneAt(q0 - 1)
   335  			if !runes.IsFilename(c) && !runes.IsAddr(c) && !runes.IsRegx(c) {
   336  				break
   337  			}
   338  			q0--
   339  			if colon < 0 && c == ':' && !texthas(t, q0-4, []rune("http://")) && !texthas(t, q0-5, []rune("https://")) {
   340  				colon = q0
   341  			}
   342  		}
   343  		/*
   344  		 * if it looks like it might begin file: , consume address chars after :
   345  		 * otherwise terminate expansion at :
   346  		 */
   347  		if colon >= 0 {
   348  			q1 = colon
   349  			if colon < t.Len()-1 && runes.IsAddr(t.RuneAt(colon+1)) {
   350  				q1 = colon + 1
   351  				for q1 < t.Len() && runes.IsAddr(t.RuneAt(q1)) {
   352  					q1++
   353  				}
   354  			}
   355  		}
   356  		if q1 > q0 {
   357  			if colon >= 0 { // stop at white space
   358  				for amax = colon + 1; amax < t.Len(); amax++ {
   359  					c = t.RuneAt(amax)
   360  					if c == ' ' || c == '\t' || c == '\n' {
   361  						break
   362  					}
   363  				}
   364  			} else {
   365  				amax = t.Len()
   366  			}
   367  		}
   368  	}
   369  	amin := amax
   370  	e.Q0 = q0
   371  	e.Q1 = q1
   372  	n := q1 - q0
   373  	if n == 0 {
   374  		return false
   375  	}
   376  	// see if it's a file name
   377  	r := make([]rune, n)
   378  	t.File.Read(q0, r)
   379  	// is it a URL? look for http:// and https:// prefix
   380  	if hasPrefix(r, []rune("http://")) || hasPrefix(r, []rune("https://")) {
   381  		// Avoid capturing end-of-sentence punctuation.
   382  		if r[n-1] == '.' {
   383  			e.Q1--
   384  			n--
   385  		}
   386  		e.Name = r
   387  		e.Arg = t
   388  		e.A0 = e.Q1
   389  		e.A1 = e.Q1
   390  		return true
   391  	}
   392  	// first, does it have bad chars?
   393  	nname := -1
   394  	var i int
   395  	for i = 0; i < n; i++ {
   396  		c = r[i]
   397  		if c == ':' && nname < 0 {
   398  			if q0+i+1 < t.Len() && (i == n-1 || runes.IsAddr(t.RuneAt(q0+i+1))) {
   399  				amin = q0 + i
   400  			} else {
   401  				return false
   402  			}
   403  			nname = i
   404  		}
   405  	}
   406  	if nname == -1 {
   407  		nname = n
   408  	}
   409  	for i = 0; i < nname; i++ {
   410  		if !runes.IsFilename(r[i]) && r[i] != ' ' {
   411  			return false
   412  		}
   413  	}
   414  	/*
   415  	 * See if it's a file name in <>, and turn that into an include
   416  	 * file name if so.  Should probably do it for "" too, but that's not
   417  	 * restrictive enough syntax and checking for a #include earlier on the
   418  	 * line would be silly.
   419  	 */
   420  	if q0 > 0 && t.RuneAt(q0-1) == '<' && q1 < t.Len() && t.RuneAt(q1) == '>' {
   421  		rs := includename(t, r[:nname])
   422  		r = rs
   423  		nname = len(rs)
   424  	} else if amin == q0 {
   425  		goto Isfile
   426  	} else {
   427  		rs := wind.Dirname(t, r[:nname])
   428  		r = rs
   429  		nname = len(rs)
   430  	}
   431  	e.Bname = string(r[:nname])
   432  	// if it's already a window name, it's a file
   433  	{
   434  		w := LookFile(r[:nname])
   435  		if w != nil {
   436  			goto Isfile
   437  		}
   438  		// if it's the name of a file, it's a file
   439  		if Ismtpt(e.Bname) {
   440  			e.Bname = ""
   441  			return false
   442  		}
   443  		if _, err := os.Stat(e.Bname); err != nil {
   444  			e.Bname = ""
   445  			return false
   446  		}
   447  	}
   448  
   449  Isfile:
   450  	e.Name = r[:nname]
   451  	e.Arg = t
   452  	e.A0 = amin + 1
   453  	eval := false
   454  	addr.Eval(true, nil, runes.Rng(-1, -1), runes.Rng(0, 0), t, e.A0, amax, tgetc, &eval, (*int)(&e.A1))
   455  	return true
   456  }
   457  
   458  func Expand_(t *wind.Text, q0 int, q1 int, e *Expand) bool {
   459  	*e = Expand{}
   460  	e.Agetc = tgetc
   461  	// if in selection, choose selection
   462  	e.Jump = true
   463  	if q1 == q0 && t.Q1 > t.Q0 && t.Q0 <= q0 && q0 <= t.Q1 {
   464  		q0 = t.Q0
   465  		q1 = t.Q1
   466  		if t.What == wind.Tag {
   467  			e.Jump = false
   468  		}
   469  	}
   470  
   471  	if expandfile(t, q0, q1, e) {
   472  		return true
   473  	}
   474  
   475  	if q0 == q1 {
   476  		for q1 < t.Len() && runes.IsAlphaNum(t.RuneAt(q1)) {
   477  			q1++
   478  		}
   479  		for q0 > 0 && runes.IsAlphaNum(t.RuneAt(q0-1)) {
   480  			q0--
   481  		}
   482  	}
   483  	e.Q0 = q0
   484  	e.Q1 = q1
   485  	return q1 > q0
   486  }
   487  
   488  func LookFile(s []rune) *wind.Window {
   489  	// avoid terminal slash on directories
   490  	if len(s) > 0 && s[len(s)-1] == '/' {
   491  		s = s[:len(s)-1]
   492  	}
   493  	for _, c := range wind.TheRow.Col {
   494  		for _, w := range c.W {
   495  			t := &w.Body
   496  			k := len(t.File.Name())
   497  			if k > 1 && t.File.Name()[k-1] == '/' {
   498  				k--
   499  			}
   500  			if runes.Equal(t.File.Name()[:k], s) {
   501  				w = w.Body.File.Curtext.W
   502  				if w.Col != nil { // protect against race deleting w
   503  					return w
   504  				}
   505  			}
   506  		}
   507  	}
   508  	return nil
   509  }
   510  
   511  func LookID(id int) *wind.Window {
   512  	for _, c := range wind.TheRow.Col {
   513  		for _, w := range c.W {
   514  			if w.ID == id {
   515  				return w
   516  			}
   517  		}
   518  	}
   519  	return nil
   520  }
   521  
   522  type Expand struct {
   523  	Q0    int
   524  	Q1    int
   525  	Name  []rune
   526  	Bname string
   527  	Jump  bool
   528  	Arg   interface{}
   529  	Agetc func(interface{}, int) rune
   530  	A0    int
   531  	A1    int
   532  }
   533  
   534  func tgetc(a interface{}, n int) rune {
   535  	t := a.(*wind.Text)
   536  	if n >= t.Len() {
   537  		return 0
   538  	}
   539  	return t.RuneAt(n)
   540  }
   541  
   542  var Ismtpt func(string) bool
   543  
   544  var Plumbsendfid *client.Fid
   545  
   546  var Wdir = "."
   547  
   548  var Objtype string
   549  
   550  func Openfile(t *wind.Text, e *Expand) *wind.Window {
   551  	var r runes.Range
   552  	r.Pos = 0
   553  	r.End = 0
   554  	var w *wind.Window
   555  	if len(e.Name) == 0 {
   556  		w = t.W
   557  		if w == nil {
   558  			return nil
   559  		}
   560  	} else {
   561  		w = LookFile(e.Name)
   562  		if w == nil && e.Name[0] != '/' {
   563  			/*
   564  			 * Unrooted path in new window.
   565  			 * This can happen if we type a pwd-relative path
   566  			 * in the topmost tag or the column tags.
   567  			 * Most of the time plumber takes care of these,
   568  			 * but plumber might not be running or might not
   569  			 * be configured to accept plumbed directories.
   570  			 * Make the name a full path, just like we would if
   571  			 * opening via the plumber.
   572  			 */
   573  			rs := []rune(path.Join(string(Wdir), string(e.Name)))
   574  			e.Name = rs
   575  			w = LookFile(e.Name)
   576  		}
   577  	}
   578  	if w != nil {
   579  		t = &w.Body
   580  		if !t.Col.Safe && t.Fr.MaxLines == 0 { // window is obscured by full-column window
   581  			wind.Colgrow(t.Col, t.Col.W[0], 1)
   582  		}
   583  	} else {
   584  		var ow *wind.Window
   585  		if t != nil {
   586  			ow = t.W
   587  		}
   588  		w = Makenewwindow(t)
   589  		t = &w.Body
   590  		wind.Winsetname(w, e.Name)
   591  		if Textload(t, 0, e.Bname, true) >= 0 {
   592  			t.File.Unread = false
   593  		}
   594  		t.File.SetMod(false)
   595  		t.W.Dirty = false
   596  		wind.Winsettag(t.W)
   597  		wind.Textsetselect(&t.W.Tag, t.W.Tag.Len(), t.W.Tag.Len())
   598  		if ow != nil {
   599  			for i := len(ow.Incl); ; {
   600  				i--
   601  				if i < 0 {
   602  					break
   603  				}
   604  				rp := runes.Clone(ow.Incl[i])
   605  				wind.Winaddincl(w, rp)
   606  			}
   607  			w.Autoindent = ow.Autoindent
   608  		} else {
   609  			w.Autoindent = wind.GlobalAutoindent
   610  		}
   611  		OnNewWindow(w)
   612  	}
   613  	var eval bool
   614  	if e.A1 == e.A0 {
   615  		eval = false
   616  	} else {
   617  		eval = true
   618  		var dummy int
   619  		r = addr.Eval(true, t, runes.Rng(-1, -1), runes.Rng(t.Q0, t.Q1), e.Arg, e.A0, e.A1, e.Agetc, &eval, &dummy)
   620  		if r.Pos > r.End {
   621  			eval = false
   622  			alog.Printf("addresses out of order\n")
   623  		}
   624  		if !eval {
   625  			e.Jump = false // don't jump if invalid address
   626  		}
   627  	}
   628  	if !eval {
   629  		r.Pos = t.Q0
   630  		r.End = t.Q1
   631  	}
   632  	wind.Textshow(t, r.Pos, r.End, true)
   633  	wind.Winsettag(t.W)
   634  	wind.Seltext = t
   635  	if e.Jump {
   636  		adraw.Display.MoveCursor(t.Fr.PointOf(t.Fr.P0).Add(draw.Pt(4, adraw.Font.Height-4)))
   637  	}
   638  	return w
   639  }
   640  
   641  var Textload func(*wind.Text, int, string, bool) int
   642  
   643  var OnNewWindow func(*wind.Window)