func (t *tbfe) renderthread() { pc := 0 dorender := func() { defer func() { if r := recover(); r != nil { log.Errorf("Panic in renderthread: %v\n%s", r, string(debug.Stack())) if pc > 1 { panic(r) } pc++ } }() termbox.Clear(defaultFg, defaultBg) t.lock.Lock() vs := make([]*backend.View, 0, len(t.layout)) ls := make([]layout, 0, len(t.layout)) for v, l := range t.layout { vs = append(vs, v) ls = append(ls, l) } t.lock.Unlock() for i, v := range vs { t.renderView(v, ls[i]) } termbox.Flush() } for range t.dorender { dorender() } }
func GetEditor() *Editor { edl.Lock() defer edl.Unlock() if ed == nil { ed = &Editor{ cmdHandler: commandHandler{ ApplicationCommands: make(appcmd), TextCommands: make(textcmd), WindowCommands: make(wndcmd), verbose: true, }, frontend: &DummyFrontend{}, console: &View{ buffer: NewBuffer(), scratch: true, }, keyInput: make(chan keys.KeyPress, 32), } var err error if ed.Watcher, err = watch.NewWatcher(); err != nil { log.Errorf("Couldn't create watcher: %s", err) } ed.console.Settings().Set("is_widget", true) ed.defaultSettings = new(HasSettings) ed.platformSettings = new(HasSettings) ed.Settings() // Just to initialize it ed.defaultBindings = new(keys.HasKeyBindings) ed.platformBindings = new(keys.HasKeyBindings) ed.userBindings = new(keys.HasKeyBindings) log.AddFilter("console", log.DEBUG, log.NewLogWriter(ed.handleLog)) go ed.inputthread() go ed.Observe() } return ed }
func onInit() { l := py.NewLock() defer l.Unlock() m, err := py.Import("sublime_plugin") if err != nil { panic(err) } sys, err := py.Import("sys") if err != nil { log.Debug(err) } else { defer sys.Decref() } if watcher, err = watch.NewWatcher(); err != nil { log.Errorf("Couldn't create watcher: %s", err) } // TODO: add all plugins after supporting all commands // plugins := packages.ScanPlugins(backend.LIME_PACKAGES_PATH, ".py") // for _, p := range plugins { // newPlugin(p, m) // } newPlugin(packages.NewPlugin(path.Join(backend.LIME_PACKAGES_PATH, "Vintageous"), ".py"), m) go watcher.Observe() }
func (w *Window) OpenFile(filename string, flags int) *View { v := w.NewFile() v.SetScratch(true) e := v.BeginEdit() if fn, err := filepath.Abs(filename); err != nil { v.Buffer().SetFileName(filename) } else { v.Buffer().SetFileName(fn) } if d, err := ioutil.ReadFile(filename); err != nil { log.Errorf("Couldn't load file %s: %s", filename, err) } else { v.Insert(e, 0, string(d)) } v.EndEdit(e) v.selection.Clear() v.selection.Add(text.Region{A: 0, B: 0}) v.Settings().Set("lime.last_save_change_count", v.buffer.ChangeCount()) v.SetScratch(false) OnLoad.Call(v) w.SetActiveView(v) return v }
func (e *Editor) SetClipboard(n string) { if err := e.clipboardSetter(n); err != nil { log.Errorf("Could not set clipboard: %v", err) } // Keep a local copy in case the system clipboard isn't working e.clipboard = n }
func register(cmds []backend.Command) { ch := backend.GetEditor().CommandHandler() for _, cmd := range cmds { if err := ch.RegisterWithDefault(cmd); err != nil { log.Errorf("Failed to register command: %s", err) } } }
func registerByName(cmds []namedCmd) { ch := backend.GetEditor().CommandHandler() for _, cmd := range cmds { if err := ch.Register(cmd.name, cmd.cmd); err != nil { log.Errorf("Failed to register command %s: %s", cmd.name, err) } } }
func (w *Window) runCommand(c WindowCommand, name string) error { defer func() { if r := recover(); r != nil { log.Errorf("Paniced while running window command %s %v: %v\n%s", name, c, r, string(debug.Stack())) } }() return c.Run(w) }
func setColorMode() { var ( mode256 bool pal = make([]termbox.RGB, 0, 256) ) if err := termbox.SetColorMode(termbox.ColorMode256); err != nil { log.Errorf("Unable to use 256 color mode: %s", err) } else { log.Debug("Using 256 color mode") mode256 = true } if !mode256 { pal = pal[:10] // Not correct, but whatever pal[termbox.ColorBlack] = termbox.RGB{R: 0, G: 0, B: 0} pal[termbox.ColorWhite] = termbox.RGB{R: 255, G: 255, B: 255} pal[termbox.ColorRed] = termbox.RGB{R: 255, G: 0, B: 0} pal[termbox.ColorGreen] = termbox.RGB{R: 0, G: 255, B: 0} pal[termbox.ColorBlue] = termbox.RGB{R: 0, G: 0, B: 255} pal[termbox.ColorMagenta] = termbox.RGB{R: 255, G: 0, B: 255} pal[termbox.ColorYellow] = termbox.RGB{R: 255, G: 255, B: 0} pal[termbox.ColorCyan] = termbox.RGB{R: 0, G: 255, B: 255} diff := func(i, j byte) int { v := int(i) - int(j) if v < 0 { return -v } return v } palLut = func(col textmate.Color) termbox.Attribute { mindist := 10000000 mini := 0 for i, c := range pal { if dist := diff(c.R, col.R) + diff(c.G, col.G) + diff(c.B, col.B); dist < mindist { mindist = dist mini = i } } return termbox.Attribute(mini) } } else { palLut = func(col textmate.Color) termbox.Attribute { tc := termbox.RGB{R: col.R, G: col.G, B: col.B} for i, c := range pal { if c == tc { return termbox.Attribute(i) } } l := len(pal) log.Debug("Adding colour: %d %+v %+v", l, col, tc) pal = append(pal, tc) termbox.SetColorPalette(pal) return termbox.Attribute(l) } } }
func (e *Editor) GetClipboard() string { if n, err := e.clipboardGetter(); err == nil { return n } else { log.Errorf("Could not get clipboard: %v", err) } return e.clipboard }
func (p *plugin) load(pkg *packages.Packet) { if err := pkg.Load(); err != nil { log.Errorf("Failed to load packet %s: %s", pkg.Name(), err) } else { log.Info("Loaded %s", pkg.Name()) if err := watcher.Watch(pkg.Name(), pkg); err != nil { log.Warn("Couldn't watch %s: %s", pkg.Name(), err) } } }
func newPlugin(pl *packages.Plugin, m *py.Module) (p *plugin) { p = &plugin{pl, m} p.FileChanged(p.Name()) if err := watcher.Watch(p.Name(), p); err != nil { log.Errorf("Couldn't watch %s: %s", p.Name(), err) } p.loadKeyBindings() p.loadSettings() return }
// Put back watchers on watching files under the directory func (w *Watcher) removeDir(name string) { for p, _ := range w.watched { if filepath.Dir(p) == name { if err := w.watch(p); err != nil { log.Errorf("Could not watch: %s", err) continue } } } w.dirs = remove(w.dirs, name) }
// On plugin reload we will scan for plugin files // and packets in plugin path func (p *Plugin) Reload() { var files []os.FileInfo log.Info("Reloading plugin %s", p.Name()) f, err := os.Open(p.path) if err != nil { log.Errorf("Couldn't open dir: %s", err) return } defer f.Close() fi, err := f.Readdir(-1) if err != nil { log.Errorf("Couldn't read dir: %s", err) return } for _, f := range fi { if p.suffix != "" && strings.HasSuffix(f.Name(), p.suffix) { files = append(files, f) } } p.files = files }
// Remove watchers created on files under this directory because // one watcher on the parent directory is enough for all of them func (w *Watcher) flushDir(name string) { if exist(w.dirs, name) { return } w.dirs = append(w.dirs, name) for _, p := range w.watchers { if filepath.Dir(p) == name && !exist(w.dirs, p) { if err := w.removeWatch(p); err != nil { log.Errorf("Couldn't unwatch file %s: %s", p, err) } } } }
// Observe dispatches notifications received by the watcher. This function will // return when the watcher is closed. func (w *Watcher) Observe() { for { select { case ev, ok := <-w.wchr.Events: if !ok { break } func() { w.lock.Lock() defer w.lock.Unlock() w.apply(ev) name := ev.Name // If the name refers to a directory run all watched // callbacks for wathed files under the directory if exist(w.dirs, name) { for p, _ := range w.watched { if filepath.Dir(p) == name { ev.Name = p w.apply(ev) } } } dir := filepath.Dir(name) // The watcher will be removed if the file is deleted // so we need to watch the parent directory for when the // file is created again if ev.Op&fsnotify.Remove != 0 { w.watchers = remove(w.watchers, name) w.lock.Unlock() w.Watch(dir, nil) w.lock.Lock() } // We will apply parent directory FileChanged callbacks to, // if one of the files inside the directory has changed if cbs, exist := w.watched[dir]; ev.Op&fsnotify.Write != 0 && exist { for _, cb := range cbs { if c, ok := cb.(FileChangedCallback); ok { c.FileChanged(dir) } } } }() case err, ok := <-w.wchr.Errors: if !ok { break } log.Errorf("Watcher error: %s", err) } } }
func (w *Window) remove(v *View) { w.lock.Lock() defer w.lock.Unlock() for i, vv := range w.views { if v == vv { end := len(w.views) - 1 if i != end { copy(w.views[i:], w.views[i+1:]) } w.views = w.views[:end] return } } log.Errorf("Wanted to remove view %+v, but it doesn't appear to be a child of this window", v) }
func (e *Editor) remove(w *Window) { edl.Lock() defer edl.Unlock() for i, ww := range e.windows { if w == ww { end := len(e.windows) - 1 if i != end { copy(e.windows[i:], e.windows[i+1:]) } e.windows = e.windows[:end] return } } log.Errorf("Wanted to remove window %+v, but it doesn't appear to be a child of this editor", w) }
func (v *View) runCommand(cmd TextCommand, name string) error { e := v.BeginEdit() e.command = name // e.args = args e.bypassUndo = cmd.BypassUndo() defer func() { v.EndEdit(e) if r := recover(); r != nil { log.Errorf("Paniced while running text command %s %v: %v\n%s", name, cmd, r, string(debug.Stack())) } }() p := Prof.Enter("view.cmd." + name) defer p.Exit() return cmd.Run(v, e) }
func (lp *LanguageParser) Parse() (*parser.Node, error) { sdata := string(lp.data) rn := parser.Node{P: lp, Name: lp.l.ScopeName} defer func() { if r := recover(); r != nil { log.Errorf("Panic during parse: %v\n", r) log.Debug("%v", rn) } }() iter := maxiter for i := 0; i < len(sdata) && iter > 0; iter-- { pat, ret := lp.l.RootPattern.Cache(sdata, i) nl := strings.IndexAny(sdata[i:], "\n\r") if nl != -1 { nl += i } if ret == nil { break } else if nl > 0 && nl <= ret[0] { i = nl for i < len(sdata) && (sdata[i] == '\n' || sdata[i] == '\r') { i++ } } else { n := pat.CreateNode(sdata, i, lp, ret) rn.Append(n) i = n.Range.B } } rn.UpdateRange() if len(sdata) != 0 { lut := make([]int, len(sdata)+1) j := 0 for i := range sdata { lut[i] = j j++ } lut[len(sdata)] = len(lp.data) lp.patch(lut, &rn) } if iter == 0 { panic("reached maximum number of iterations") } return &rn, nil }
// Returns packet file data if any error occurred // on reading file we will return nil func (p *Packet) Get() interface{} { e := []byte(`{}`) if p.group() == "keymap" { e = []byte(`[]`) } if _, err := os.Stat(p.path); os.IsNotExist(err) { log.Finest("%s doesn't exist yet", p.path) return e } d, err := ioutil.ReadFile(p.path) if err != nil { log.Errorf("Couldn't read file: %s", err) return e } return d }
func (v *View) FileChanged(filename string) { log.Finest("Reloading %s", filename) if saving, ok := v.Settings().Get("lime.saving", false).(bool); ok && saving { // This reload was triggered by ourselves saving to this file, so don't reload it return } if !GetEditor().Frontend().OkCancelDialog("File was changed by another program, reload?", "reload") { return } if d, err := ioutil.ReadFile(filename); err != nil { log.Errorf("Could not read file: %s\n. Error was: %v", filename, err) } else { edit := v.BeginEdit() end := v.buffer.Size() v.Replace(edit, Region{0, end}, string(d)) v.EndEdit(edit) } }
func (t *tbfe) render(w io.Writer) { defer func() { if r := recover(); r != nil { log.Errorf("Panic in renderthread: %v\n%s", r, string(debug.Stack())) if pc > 1 { panic(r) } pc++ } }() vs := make([]*backend.View, 0, len(t.layout)) l := make([]layout, 0, len(t.layout)) for k, v := range t.layout { vs = append(vs, k) l = append(l, v) } for i, v := range vs { t.renderView(w, v, l[i]) } // runes := []rune(t.status_message) }
func (q *qmlDialog) Show(msg, icon string) (ret int) { src := `import QtQuick 2.2 import QtQuick.Dialogs 1.1 Item {MessageDialog { objectName: "realDialog" id: messageDialog title: "May I have your attention please" text: "` + msg + `" icon: ` + icon + ` standardButtons: StandardButton.Ok | StandardButton.Cancel Component.onCompleted: visible = true }}` engine := qml.NewEngine() engine.Context().SetVar("q", q) component, err := engine.LoadString("dialog.qml", src) if err != nil { log.Errorf("Unable to instanciate dialog: %s", err) return 0 } var wg sync.WaitGroup wg.Add(1) obj := component.Create(nil) obj = obj.ObjectByName("realDialog") obj.On("accepted", func() { ret = 1 wg.Done() }) obj.On("rejected", func() { ret = 0 wg.Done() }) wg.Wait() engine.Destroy() log.Debug("returning %d", ret) return }
// parsethread() would be the go-routine used for dealing with reparsing the // current buffer when it has been modified. Each opened view has its own // go-routine parsethread() which sits idle and waits for requests to be sent // on this view's reparseChan. // // The Buffer's ChangeCount, as well as the parse request's "forced" attribute // is used to determined if a parse actually needs to happen or not. // // If it is decided that a reparse should take place, a snapshot of the Buffer is // taken and a parse is performed. Upon completion of this parse operation, // and if the snapshot of the buffer has not already become outdated, // then the regions of the view associated with syntax highlighting is updated. // // Changes made to the Buffer during the time when there is no accurate // parse of the buffer is a monkey-patched version of the old syntax highlighting // regions, which in most instances will be accurate. // // See package lime-backend/lib/parser for more details. func (v *View) parsethread() { pc := 0 lastParse := -1 doparse := func() (ret bool) { p := Prof.Enter("syntax.parse") defer p.Exit() defer func() { if r := recover(); r != nil { log.Errorf("Panic in parse thread: %v\n%s", r, string(debug.Stack())) if pc > 0 { panic(r) } pc++ } }() b := v.buffer sub := b.Substr(Region{0, b.Size()}) source, _ := v.Settings().Get("syntax", "").(string) if len(source) == 0 { return } // TODO: Allow other parsers instead of this hardcoded textmate version pr, err := textmate.NewLanguageParser(source, sub) if err != nil { log.Errorf("Couldn't parse: %v", err) return } syn, err := parser.NewSyntaxHighlighter(pr) if err != nil { log.Errorf("Couldn't create syntaxhighlighter: %v", err) return } // Only set if it isn't invalid already, otherwise the // current syntax highlighting will be more accurate // as it will have had incremental adjustments done to it if v.buffer.ChangeCount() != lastParse { return } v.lock.Lock() defer v.lock.Unlock() v.syntax = syn for k := range v.regions { if strings.HasPrefix(k, "lime.syntax") { delete(v.regions, k) } } for k, v2 := range syn.Flatten() { if v2.Regions.HasNonEmpty() { v.regions[k] = v2 } } return true } v.lock.Lock() ch := v.reparseChan v.lock.Unlock() defer v.cleanup() if ch == nil { return } for pr := range ch { if cc := v.buffer.ChangeCount(); lastParse != cc || pr.forced { lastParse = cc if doparse() { v.Settings().Set("lime.syntax.updated", lastParse) } } } }
func (e *Editor) inputthread() { pc := 0 var lastBindings keys.KeyBindings doinput := func(kp keys.KeyPress) { defer func() { if r := recover(); r != nil { log.Errorf("Panic in inputthread: %v\n%s", r, string(debug.Stack())) if pc > 0 { panic(r) } pc++ } }() p := Prof.Enter("hi") defer p.Exit() lvl := log.FINE if e.logInput { lvl++ } log.Logf(lvl, "Key: %v", kp) if lastBindings.SeqIndex() == 0 { lastBindings = *e.KeyBindings() } try_again: possible_actions := lastBindings.Filter(kp) lastBindings = possible_actions // TODO? var ( wnd *Window v *View ) if wnd = e.ActiveWindow(); wnd != nil { v = wnd.ActiveView() } qc := func(key string, operator Op, operand interface{}, match_all bool) bool { return OnQueryContext.Call(v, key, operator, operand, match_all) == True } if action := possible_actions.Action(qc); action != nil { p2 := Prof.Enter("hi.perform") e.RunCommand(action.Command, action.Args) p2.Exit() } else if possible_actions.SeqIndex() > 1 { // TODO: this disables having keyBindings with more than 2 key sequence lastBindings = *e.KeyBindings() goto try_again } else if kp.IsCharacter() { p2 := Prof.Enter("hi.character") log.Finest("[editor.inputthread] kp: |%s|, pos: %v", kp.Text, possible_actions) if err := e.CommandHandler().RunTextCommand(v, "insert", Args{"characters": kp.Text}); err != nil { log.Debug("Couldn't run textcommand: %s", err) } p2.Exit() } } for kp := range e.keyInput { doinput(kp) } }
func (t *qmlfrontend) loop() (err error) { backend.OnNew.Add(t.onNew) backend.OnClose.Add(t.onClose) backend.OnLoad.Add(t.onLoad) backend.OnSelectionModified.Add(t.onSelectionModified) ed := backend.GetEditor() ed.SetFrontend(t) ed.LogInput(false) ed.LogCommands(false) c := ed.Console() t.Console = &frontendView{bv: c} c.Buffer().AddObserver(t.Console) c.Buffer().AddObserver(t) go ed.Init() var ( engine *qml.Engine component qml.Object // WaitGroup keeping track of open windows wg sync.WaitGroup ) // create and setup a new engine, destroying // the old one if one exists. // // This is needed to re-load qml files to get // the new file contents from disc as otherwise // the old file would still be what is referenced. newEngine := func() (err error) { if engine != nil { log.Debug("calling destroy") // TODO(.): calling this appears to make the editor *very* crash-prone, just let it leak for now // engine.Destroy() engine = nil } log.Debug("calling newEngine") engine = qml.NewEngine() engine.On("quit", t.Quit) log.Debug("setvar frontend") engine.Context().SetVar("frontend", t) log.Debug("setvar editor") engine.Context().SetVar("editor", backend.GetEditor()) log.Debug("loadfile") component, err = engine.LoadFile(qmlMainFile) if err != nil { return err } limeViewComponent, err = engine.LoadFile(qmlViewFile) return } if err := newEngine(); err != nil { log.Error(err) } backend.OnNewWindow.Add(func(w *backend.Window) { fw := &frontendWindow{bw: w} t.windows[w] = fw if component != nil { fw.launch(&wg, component) } }) // TODO: should be done backend side if sc, err := textmate.LoadTheme("../packages/themes/TextMate-Themes/Monokai.tmTheme"); err != nil { log.Error(err) } else { scheme = sc } defer func() { fmt.Println(util.Prof) }() w := ed.NewWindow() v := w.OpenFile("main.go", 0) // TODO: should be done backend side v.SetSyntaxFile("../packages/go.tmbundle/Syntaxes/Go.tmLanguage") watch, err := fsnotify.NewWatcher() if err != nil { log.Errorf("Unable to create file watcher: %s", err) return } defer watch.Close() watch.Watch(".") defer watch.RemoveWatch(".") reloadRequested := false go func() { for { select { case ev := <-watch.Event: if ev != nil && strings.HasSuffix(ev.Name, ".qml") && ev.IsModify() && !ev.IsAttrib() { reloadRequested = true t.Quit() } } } }() for { // Reset reload status reloadRequested = false log.Debug("Waiting for all windows to close") // wg would be the WaitGroup all windows belong to, so first we wait for // all windows to close. wg.Wait() log.Debug("All windows closed. reloadRequest: %v", reloadRequested) // then we check if there's a reload request in the pipe if !reloadRequested || len(t.windows) == 0 { // This would be a genuine exit; all windows closed by the user break } // *We* closed all windows because we want to reload freshly changed qml // files. for { log.Debug("Calling newEngine") if err := newEngine(); err != nil { // Reset reload status reloadRequested = false log.Error(err) for !reloadRequested { // This loop allows us to re-try reloading // if there was an error in the file this time, // we just loop around again when we receive the next // reload request (ie on the next save of the file). time.Sleep(time.Second) } continue } log.Debug("break") break } log.Debug("re-launching all windows") // Succeeded loading the file, re-launch all windows for _, v := range t.windows { v.launch(&wg, component) } } return }
func (t *tbfe) loop() { backend.OnNew.Add(func(v *backend.View) { v.Settings().AddOnChange("lime.frontend.html.render", func(name string) { if name != "lime.syntax.updated" { return } t.SetDirty() }) }) // TODO: maybe not useful? /*backend.OnModified.Add(func(v *backend.View) { t.SetDirty() })*/ backend.OnSelectionModified.Add(func(v *backend.View) { t.BroadcastData(t.GetSelectionMessage(v)) }) ed := backend.GetEditor() ed.SetFrontend(t) ed.LogInput(false) ed.LogCommands(false) c := ed.Console() if sc, err := textmate.LoadTheme(path.Join(backend.LIME_PACKAGES_PATH, "themes", "TextMate-Themes", "Monokai.tmTheme")); err != nil { log.Error(err) } else { scheme = sc } defer func() { fmt.Println(util.Prof) }() w := ed.NewWindow() v := w.OpenFile("main.go", 0) //v.Settings().Set("trace", true) v.Settings().Set("syntax", path.Join(backend.LIME_PACKAGES_PATH, "go.tmbundle", "Syntaxes", "Go.tmLanguage")) c.Buffer().AddObserver(t) sel := v.Sel() sel.Clear() // end := v.Buffer().Size() - 2 sel.Add(Region{0, 0}) // sel.Add(Region{end - 22, end - 22}) // sel.Add(Region{end - 16, end - 20}) // sel.Add(Region{end - 13, end - 10}) { w, h := 800, 600 t.lock.Lock() t.layout[v] = layout{0, 0, w, h - console_height - 1, Region{}, 0} t.layout[c] = layout{0, h - console_height + 1, w, console_height - 5, Region{}, 0} t.lock.Unlock() t.Show(v, Region{1, 1}) } t.Show(v, Region{100, 100}) t.Show(v, Region{1, 1}) go ed.Init() log.Debug("Serving on port %d", *port) http.HandleFunc("/", t.ServeHTTP) http.HandleFunc("/view", t.view) http.HandleFunc("/key", t.key) http.HandleFunc("/themes/", t.theme) http.Handle("/ws", websocket.Handler(t.WebsocketServer)) if err := http.ListenAndServe(fmt.Sprintf("localhost:%d", *port), nil); err != nil { log.Errorf("Error serving: %s", err) } log.Debug("Done") }
// Ends the given Edit object. func (v *View) EndEdit(edit *Edit) { if edit.invalid { // This happens when nesting Edits and the child Edit ends after the parent edit. log.Fine("This edit has already been invalidated: %v, %v", edit, v.editstack) return } // Find the position of this Edit object in this View's Edit stack. // If plugins, commands, etc are well-behaved the ended edit should be // last in the stack, but shit happens and we cannot count on this being the case. i := len(v.editstack) - 1 for i := len(v.editstack) - 1; i >= 0; i-- { if v.editstack[i] == edit { break } } if i == -1 { // TODO(.): Under what instances does this happen again? log.Errorf("This edit isn't even in the stack... where did it come from? %v, %v", edit, v.editstack) return } var selection_modified bool if l := len(v.editstack) - 1; i != l { // TODO(.): See TODO in BeginEdit log.Errorf("This edit wasn't last in the stack... %d != %d: %v, %v", i, l, edit, v.editstack) } // Invalidate all Edits "below" and including this Edit. for j := len(v.editstack) - 1; j >= i; j-- { current_edit := v.editstack[j] current_edit.invalid = true sel_same := reflect.DeepEqual(*v.Sel(), current_edit.savedSel) buf_same := v.buffer.ChangeCount() == current_edit.savedCount eq := (sel_same && buf_same && current_edit.composite.Len() == 0) if !eq && !sel_same { selection_modified = true } if v.IsScratch() || current_edit.bypassUndo || eq { continue } switch { case i == 0: // Well-behaved, no nested edits! fallthrough case j != i: // BOO! Someone began another Edit without finishing the first one. // In this instance, the parent Edit ended before the child. // TODO(.): What would be the correct way to handle this? v.undoStack.Add(edit) default: // BOO! Also poorly-behaved. This Edit object began after the parent began, // but was finished before the parent finished. // // Add it as a child of the parent Edit so that undoing the parent // will undo this edit as well. v.editstack[i-1].composite.Add(current_edit) } } // Pop this Edit and all the children off the Edit stack. v.editstack = v.editstack[:i] if selection_modified { OnSelectionModified.Call(v) } }