ref: 82b046f36f8084a22bbb5d71edd0edd9179561eb
dir: /appl/wm/edit.b/
# # Copyright © 1996-1999 Lucent Technologies Inc. All rights reserved. # Modified version of edit # D.B.Knudsen # Revisions Copyright © 2000-2002 Vita Nuova Holdings Limited. All rights reserved. # implement WmEdit; include "sys.m"; sys: Sys; include "draw.m"; draw: Draw; Rect, Screen: import draw; include "tk.m"; tk: Tk; include "tkclient.m"; tkclient: Tkclient; include "dialog.m"; dialog: Dialog; include "selectfile.m"; selectfile: Selectfile; WmEdit: module { init: fn(ctxt: ref Draw->Context, argv: list of string); }; ErrIco: con "error -fg red"; ed: ref Tk->Toplevel; dirty := 0; BLUE : con "#0000ff"; GREEN : con "#008800"; SEARCH, SEARCHFOR, REPLACE, REPLACEWITH, REPLACEALL, NOSEE : con iota; ed_config := array[] of { "frame .m -relief raised", "frame .b", "menubutton .m.file -text File -menu .m.file.menu", "menubutton .m.edit -text Edit -menu .m.edit.menu", "menubutton .m.search -text Search -menu .m.search.menu", "menubutton .m.options -text Options -menu .m.options.menu", # "label .m.filename", "pack .m.file .m.edit .m.search .m.options -side left", # "pack .m.filename -padx 10 -side left", "menu .m.file.menu", ".m.file.menu add command -label New -command {send c new}", ".m.file.menu add command -label Open... -command {send c open}", ".m.file.menu add separator", ".m.file.menu add command -label Save -command {send c save}", ".m.file.menu add command -label {Save As...} -command {send c saveas}", ".m.file.menu add separator", ".m.file.menu add command -label {Exit} -command {send c exit}", "menu .m.edit.menu", ".m.edit.menu add command -label Cut -command {send c cut}", ".m.edit.menu add command -label Copy -command {send c copy}", ".m.edit.menu add command -label Paste -command {send c paste}", "menu .m.search.menu", ".m.search.menu add command -label {Find ...} " + "-command {send c searchf}", ".m.search.menu add command -label {Replace with...} " + "-command {send c replacew}", ".m.search.menu add command -label {Find Again} -command {send c search}", ".m.search.menu add command -label {Find and Replace} " + "-command {send c replace}", ".m.search.menu add command -label {Find and Replace All} " + "-command {send c replaceall}", "menu .m.options.menu", ".m.options.menu add checkbutton -text Limbo -command {send c limbo}", ".m.options.menu add command -label Indent -command {send c indent}", "text .b.t -yscrollcommand {.b.s set} -bg white", "bind .b.t <Button-2> {.m.edit.menu post %X %Y}", "bind .b.t <Key> +{send c dirtied {%A}}", "bind .b.t <ButtonRelease-1> +{send c reindent}", "scrollbar .b.s -command {.b.t yview}", "pack .m -fill x", "pack .b.s -fill y -side left", "pack .b.t -fill both -expand 1", "pack .b -fill both -expand 1", "focus .b.t", "pack propagate . 0", ".b.t tag configure keyword -fg " + BLUE, ".b.t tag configure comment -fg " + GREEN, "update", }; context : ref Draw->Context; curfile := "(New)"; snarf := ""; searchfor := ""; replacewith := ""; path := "."; init(ctxt: ref Draw->Context, argv: list of string) { wmctl: chan of string; sys = load Sys Sys->PATH; draw = load Draw Draw->PATH; tk = load Tk Tk->PATH; tkclient = load Tkclient Tkclient->PATH; selectfile = load Selectfile Selectfile->PATH; dialog = load Dialog Dialog->PATH; sys->pctl(Sys->NEWPGRP, nil); tkclient->init(); selectfile->init(); dialog->init(); context = ctxt; (ed, wmctl) = tkclient->toplevel(context, "", "Edit", Tkclient->Appl); argv = tl argv; c := chan of string; tk->namechan(ed, c, "c"); for (i := 0; i < len ed_config; i++) cmd(ed, ed_config[i]); if (argv != nil) { e := loadtfile(hd argv); if(e != nil) dialog->prompt(ctxt, ed.image, ErrIco, "Open file", e, 0, "Ok"::nil); } tkclient->settitle(ed, "Edit " + curfile); tkclient->onscreen(ed, nil); tkclient->startinput(ed, "ptr" :: "kbd" :: nil); cmd(ed, "update"); e := cmd(ed, "variable lasterror"); if(e != "") { sys->print("edit error: %s\n", e); return; } cmdloop: for(;;) { alt { key := <-ed.ctxt.kbd => tk->keyboard(ed, key); m := <-ed.ctxt.ptr => tk->pointer(ed, *m); s := <-ed.ctxt.ctl or s = <-ed.wreq or s = <-wmctl => if(s == "exit") { if (check_dirty()) break cmdloop; else break; } task_title: string; if (s == "task") { if (curfile == "(New)") task_title = tkclient->settitle(ed, "Edit"); else task_title = tkclient->settitle(ed, "Edit " + curfile); cmd(ed, "update"); } tkclient->wmctl(ed, s); if (s == "task") tkclient->settitle(ed, task_title); s := <-c => if ( len s > 7 && s[:7] == "dirtied" ) { set_dirty(); do_limbo_check(s); } else case s { "exit" => if ( check_dirty() ){ set_clean(); break cmdloop; } "dirtied" => set_dirty(); do_limbo_check(s); "new" => if ( check_dirty()) {set_clean(); do_new();} "open" => if ( check_dirty() && do_open()) set_clean(); "save" => do_save(0); "saveas" => do_save(1); "cut" => do_snarf(1); set_dirty(); "copy" => do_snarf(0); "paste" => do_paste(); set_dirty(); "search" => do_search(SEARCH); "searchf" => do_search(SEARCHFOR); "replace" => do_replace(REPLACE); "replacew" => do_replace(REPLACEWITH); "replaceall" => do_replaceall(); "limbo" => do_limbo(); "indent" => do_indent(); "reindent" => re_indent(); } cmd(ed, "focus .b.t"); } cmd(ed, "update"); e = cmd(ed, "variable lasterror"); if(e != "") { sys->print("edit error: %s\n", e); break cmdloop; } } } check_dirty() : int { if ( dirty == 0 ) return 1; if (dialog->prompt(context, ed.image, ErrIco, "Confirm", "File was changed.\nDiscard changes?", 0, "Yes" :: "No" :: nil) == 0 ) { return 1; } return 0; } set_dirty() { if(!dirty){ dirty = 1; tkclient->settitle(ed, "Edit " + curfile + " (dirty)"); cmd(ed, "update"); } # We want to just remove the binding, but Inferno's tk does not # recognize the - in front of the command. To make it do so would # require changes to utils.c and ebind.c in /tk # cmd(ed, "bind .b.t <Key> -{send c dirtied}"); } set_clean() { if(dirty){ dirty = 0; tkclient->settitle(ed, "Edit " + curfile); cmd(ed, "update"); #cmd(ed, "bind .b.t <Key> +{send c dirtied}"); } } BLOCK, TEMP : con iota; is_limbo := 0; # initially not limbo this_word := ""; last_keyword := ""; in_comment := 0; first_char := 1; indent : list of int; last_kw_is_block := 0; tab := "\t"; tabs := array[] of { "", "\t", "\t\t", "\t\t\t", "\t\t\t\t", "\t\t\t\t\t", "\t\t\t\t\t\t", "\t\t\t\t\t\t\t", "\t\t\t\t\t\t\t\t" }; keywords := array[] of { "adt", "alt", "array", "big", "break", "byte", "case", "chan", "con", "continue", "cyclic", "do", "else", "exit", "fn", "for", "hd", "if", "implement", "import", "include", "int", "len", "list", "load", "module", "nil", "of", "or", "pick", "real", "ref", "return", "self", "spawn", "string", "tagof", "tl", "to", "type", "while" }; block_keyword := (big 1 << 40 ) | big (1 << 17) | big (1 << 15) | big (1 << 12) | big (1 << 11); do_limbo() { is_limbo = !is_limbo; if ( is_limbo ) mark_keyw_comm(); else { cmd(ed, ".b.t tag remove comment 1.0 end"); cmd(ed, ".b.t tag remove keyword 1.0 end"); } } do_limbo_check(s : string) { if ( ! is_limbo ) return; if ( len s < 11 ) return; # # Maybe we should actually remember where the insert point is. # In general we can get it via .b.t index insert, but for most # characters, we could maintain the position with simple arithmetic. # # Also, we need to insert code in cut and paste operations to keep # track of various things when in limbo mode. Also need to catch # text deletions via typeover of selection. # char := s[9]; if ( char == '\\' && len s > 10 ) char = s[10]; case char { ' ' or '\t' => if ( ! in_comment ) look_keyword(this_word); this_word = "" ; '\n' => if ( in_comment ) { # terminate current tag cmd(ed, ".b.t tag remove comment insert-1chars"); in_comment = 0; } else look_keyword(this_word); this_word = "" ; if ( last_kw_is_block ) indent = TEMP :: indent; else while ( indent != nil && hd indent == TEMP ) indent = tl indent; last_kw_is_block = 0; add_indent(); first_char = 1; return; '{' => indent = BLOCK :: indent; last_kw_is_block = 0; '}' => if ( indent != nil ) indent = tl indent; last_kw_is_block = 0; # If the line is just indentation plus '}', rewrite it # to have one less indent. if ( first_char ) { current := int cmd(ed, ".b.t index insert"); cmd(ed, ".b.t delete " + string current + ".0 insert"); add_indent(); cmd(ed, ".b.t insert insert '}"); } # ';' => # last_kw_is_block = 0; # '\b' => # By the time we see this, the character has # # already been wiped out, probably. # # To know what it was we'd need a lastchar, # # reset for each mouse button up and \b # '\u007f' => # Here, we have to know what used to be ahead of the # # insert point. '#' => # if ( ! in_quote ) { # cmd(ed, ".b.t tag add comment insert-1chars"); in_comment = 1; # } 'A' to 'Z' or 'a' to 'z' or '0' to '9' or '_' => if ( ! in_comment ) this_word[len this_word] = char; * => if ( ! in_comment ) look_keyword(this_word); this_word = ""; } if ( in_comment ) cmd(ed, ".b.t tag add comment insert-1chars"); first_char = 0; } look_keyword(word : string) { # compare this_word to all keywords if ( is_keyword(word) ) { cmd(ed, ".b.t tag add keyword insert-" + string (len this_word + 1) + "chars insert-1chars"); } } is_keyword(word : string) : int { l := len keywords; for ( i := 0; i < l; i++ ) if ( word == keywords[i] ) { if ( i != 26 ) # don't set for 'nil' last_kw_is_block = int (block_keyword >> i) & 1; return 1; } return 0; } do_new() { cmd(ed, ".b.t delete 1.0 end"); curfile = "(New)"; tkclient->settitle(ed, "Edit " + curfile); } do_open(): int { for(;;) { fname := selectfile->filename(context, ed.image, "", nil, path); if(fname == "") break; cmd(ed, ".b.t delete 1.0 end"); e := loadtfile(fname); if(e == nil) { basepath(fname); return 1; } options := list of { "Cancel", "Open another file" }; if(dialog->prompt(context, ed.image, ErrIco, "Open file", e, 0, options) == 0) break; } return 0; } basepath(file: string) { for(i := len file-1; i >= 0; i--) if(file[i] == '/') { path = file[0:i]; break; } } do_save(prompt: int) { fname := curfile; contents := tk->cmd(ed, ".b.t get 1.0 end"); for(;;) { if(prompt || curfile == "(New)") { fname = dialog->getstring(context, ed.image, "File"); if ( len fname > 0 && fname[0] != '/' && path != "" ) fname = path + "/" + fname; } if(savetfile(fname, contents)) { set_clean(); break; } options := list of { "Cancel", "Try another file" }; msg := sys->sprint("Trying to write file \"%s\"\n%r", fname); if(dialog->prompt(context, ed.image, ErrIco, "Save file", msg, 0, options) == 0) break; prompt = 1; } } do_snarf(del: int) { range := cmd(ed, ".b.t tag nextrange sel 1.0"); if(range == "" || (len range > 0 && range[0] == '!')) return; snarf = tk->cmd(ed, ".b.t get " + range); if(del) cmd(ed, ".b.t delete " + range); tkclient->snarfput(snarf); } do_paste() { snarf = tkclient->snarfget(); if(snarf == "") return; cmd(ed, ".b.t insert insert '" + snarf); } do_search(prompt: int) : int { if(prompt == SEARCHFOR) searchfor = dialog->getstring(context, ed.image, "Search For"); if(searchfor == "") return 0; cmd(ed, "cursor -bitmap cursor.wait"); ix := cmd(ed, ".b.t search -- " + tk->quote(searchfor) + " insert+1c"); if(ix != "" && len ix > 1 && ix[0] != '!') { cmd(ed, ".b.t tag remove sel 0.0 end"); cmd(ed, ".b.t mark set anchor " + ix); cmd(ed, ".b.t mark set insert " + ix); cmd(ed, ".b.t tag add sel " + ix + " " + ix + "+" + string(len searchfor) + "c"); if ( prompt != NOSEE ) cmd(ed, ".b.t see " + ix); cmd(ed, "cursor -default"); return 1; } cmd(ed, "cursor -default"); return 0; } do_replace(prompt : int) { range := ""; if ( prompt == REPLACEWITH ) { replacewith = dialog->getstring(context, ed.image, "Replacement String"); range = cmd(ed, ".b.t tag nextrange sel 1.0"); if(range == "" || (len range > 0 && range[0] == '!')) return; # nothing currently selected } if ( range != "" ) { # there's something selected cmd(ed, ".b.t mark set insert sel.first"); } else { # have to find a string if ( searchfor == "" ) { # no search string! if ( do_search(SEARCHFOR) == 0 ) return; } else if ( do_search(SEARCH) == 0 ) return; } cmd(ed, ".b.t delete sel.first sel.last"); cmd(ed, ".b.t insert insert " + tk->quote(replacewith)); } do_replaceall() { cur := cmd(ed, ".b.t index insert"); if ( cur == "" || cur[0] == '!' ) return; dirt := 0; if ( searchfor == "" ) # no search string searchfor = dialog->getstring(context, ed.image, "Search For"); if ( searchfor == "" ) # still no search string return; srch := tk->quote(searchfor); repl := tk->quote(replacewith); for ( ix := "1.0"; len ix > 0 && ix[0] != '!'; ) { ix = cmd(ed, ".b.t search -- " + srch + " " + ix + " end"); if ( ix == "" || len ix <= 1 || ix[0] == '!') break; cmd(ed, ".b.t delete " + ix + " " + ix + "+" + string(len searchfor) + "c"); if ( replacewith != "" ) { cmd(ed, ".b.t insert " + ix + " " + repl); ix = cmd(ed, ".b.t index " + ix + "+" + string(len replacewith) + "c"); } dirt++; } cmd(ed, ".b.t mark set insert " + cur); if ( dirt > 0 ) set_dirty(); } loadtfile(path: string): string { if ( path != nil && path[0] == '/' ) basepath(path); fd := sys->open(path, sys->OREAD); if(fd == nil) return "Can't open "+path+", the error was:\n"+sys->sprint("%r"); (ok, d) := sys->fstat(fd); if(ok < 0) return "Can't stat "+path+", the error was:\n"+sys->sprint("%r"); if(d.mode & Sys->DMDIR) return path+" is a directory"; cmd(ed, "cursor -bitmap cursor.wait"); BLEN: con 8192; buf := array[BLEN+Sys->UTFmax] of byte; inset := 0; for(;;) { n := sys->read(fd, buf[inset:], BLEN); if(n <= 0) break; n += inset; nutf := sys->utfbytes(buf, n); s := string buf[0:nutf]; # move any partial rune to beginning of buffer inset = n-nutf; buf[0:] = buf[nutf:n]; cmd(ed, ".b.t insert end '" + s); } if ( is_limbo ) mark_keyw_comm(); curfile = path; tkclient->settitle(ed, "Edit " + curfile); cmd(ed, "cursor -default"); cmd(ed, "update"); return ""; } savetfile(path: string, contents: string): int { buf := array of byte contents; n := len buf; fd := sys->create(path, sys->OWRITE, 8r664); if(fd == nil) return 0; i := sys->write(fd, buf, n); if(i != n) { sys->print("savetfile only wrote %d of %d: %r\n", i, n); return 0; } curfile = path; # cmd(ed, ".m.filename configure -text '" + curfile); tkclient->settitle(ed, "Edit " + curfile); return 1; } mark_keyw_comm() { quote := 0; start : int; notkey := 0; word : string; last := int cmd(ed, ".b.t index end"); for ( i := 1; i <= last; i++ ) { quote = 0; word = ""; line := tk->cmd(ed, ".b.t get " + string i + ".0 " + string (i+1) + ".0"); l := len line; ll : for ( j := 0; j < l; j++ ) { c := line[j]; if ( quote && (c = line[j]) != quote ) continue; case c { '#' => cmd(ed, sys->sprint(".b.t tag add comment" + " %d.%d %d.%d", i, j, i, l)); break ll; '\'' or '\"' => if ( j != 0 && line[j-1] == '\\' ) break; if ( c == quote ) quote = 0; else quote = line[j]; word = ""; 'a' to 'z' => if ( word == "" ) start = j; word[len word] = c; 'A' to 'Z' or '_' => notkey = 1; continue; * => if ( ! notkey && is_keyword(word) ) cmd(ed, ".b.t tag add keyword " + sys->sprint("%d.%d %d.%d", i, start, i, j)); word = ""; notkey = 0; } } } } do_indent() { for ( ; ; ) { tab = dialog->getstring(context, ed.image, "single indent"); break; } for ( i := 1; i <= 8; i++ ) { s := ""; for ( j := i; j > 0; j-- ) s += tab; tabs[i] = collapse(s); } } collapse(s : string) : string { if ( len s >= 8 && s[0:8] == " " ) return "\t" + collapse(s[8:]); return s; } add_indent() { for ( i := len indent; i >= 8; i -= 8 ) cmd(ed, ".b.t insert insert '" + tabs[8]); cmd(ed, ".b.t insert insert '" + tabs[i]); } # # We should also look at the previous line, maybe. # And the line after. That may be too much. # # This is also the logical place to check if we are in a keyword, # reinitialize this_word (which presents problems if we are in the # middle of a word, etc.) Also check if we are in a comment or not. # re_indent() { pos := cmd(ed, ".b.t index insert"); (n, lc) := sys->tokenize(pos, "."); if ( n < 2 ) return; init := tk->cmd(ed, ".b.t get " + hd lc + ".0 insert"); l := len init; for ( i := 8; i > 0; i-- ) { lt := len tabs[i]; if ( l >= lt && init[:lt] == tabs[i] ) break; } for ( indent = nil; len indent < i; indent = 0 :: indent) ; in_comment = 0; # Are we in a comment? for ( i = len tabs[i]; i < l; i++ ) if ( init[i] == '#' ) { in_comment = 1; break; } } cmd(win: ref Tk->Toplevel, s: string): string { # sys->print("%s\n", s); r := tk->cmd(win, s); if (r != nil && r[0] == '!') { sys->print("wm/edit: error executing '%s': %s\n", s, r); } return r; }