├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── debug.go ├── editor ├── command_view.go ├── commands.go ├── commands_todo.go ├── doc.go ├── document.go ├── document_view.go ├── editor.go ├── editor_test.go ├── event_registry_impl.go ├── keybindings.go ├── plugins.go ├── strings.go ├── terminal.go ├── view.go └── window.go ├── internal ├── doc.go ├── event_registry_internal.go └── interfaces.go ├── main.go ├── non_debug.go ├── terminal.go ├── tools ├── print_colors.sh ├── test.bat ├── test.sh ├── wi-event-generator │ ├── main.go │ └── parse │ │ └── parse.go └── wi.png ├── wi-plugin-sample └── main.go └── wicore ├── colors ├── colors.go └── colors_test.go ├── command.go ├── doc.go ├── event_registry_decl.go ├── interfaces.go ├── interfaces_string.go ├── key ├── key.go ├── key_string.go └── key_test.go ├── lang ├── lang.go └── lang_test.go ├── panic.go ├── plugin ├── event_registry_impl.go └── main.go ├── raster ├── raster.go └── raster_test.go ├── strings.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | /wi 2 | /wi.exe 3 | /wi.log 4 | /wi-plugin-sample/wi-plugin-sample 5 | /wi-plugin-sample/wi-plugin-sample.exe 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | # Use of this source code is governed under the Apache License, Version 2.0 3 | # that can be found in the LICENSE file. 4 | 5 | sudo: false 6 | language: go 7 | 8 | go: 9 | - 1.2 10 | - 1.4.2 11 | 12 | before_install: 13 | - go get github.com/maruel/pre-commit-go/cmd/pcg 14 | 15 | script: 16 | - pcg 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2012 Marc-Antoine Ruel 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wi - right after vi 2 | =================== 3 | 4 | The project was put on ice because it failed to attract interest and was mostly 5 | done for research purpose. Please look at 6 | [xi](https://github.com/google/xi-editor) which has similar design goals and is 7 | written in Rust instead. 8 | 9 | 10 | *Experimental, do not look into.* 11 | 12 | *Experimental, do not look into.* 13 | 14 | *Experimental, do not look into.* 15 | 16 | _Bringing text based editor technology past 1200 bauds._ 17 | 18 | 19 | [![GoDoc](https://godoc.org/github.com/wi-ed/wi?status.svg)](https://godoc.org/github.com/wi-ed/wi) 20 | [![Build Status](https://travis-ci.org/wi-ed/wi.svg?branch=master)](https://travis-ci.org/wi-ed/wi) 21 | [![Coverage Status](https://img.shields.io/coveralls/wi-ed/wi.svg)](https://coveralls.io/r/wi-ed/wi?branch=master) 22 | 23 | 24 | Features 25 | -------- 26 | 27 | - Editor for the 19.2kbps connected world. 28 | - Out of process plugins for today's 2Mb RAM systems. 29 | - [Go](https://golang.org) for both _program_ and _macros_. 30 | - Fully asynchronous processing. No hang due to I/O ever. 31 | - Extremely extensible. Everything can be overriden. 32 | - i18n ready. 33 | - Auto-generated help. 34 | - `go get` (Go's native distribution mechanism) for both the editor and 35 | plugins. 36 | - Integrated debugging and good test coverage. 37 | 38 | 39 | Setup 40 | ----- 41 | 42 | 43 | ### Prerequisites 44 | 45 | - [git](http://git-scm.com) 46 | - [Go](https://golang.org) 47 | 48 | 49 | ### Installation or updating 50 | 51 | ``` 52 | go get -u github.com/wi-ed/wi 53 | ``` 54 | 55 | 56 | ### Installing or updating a plugin 57 | 58 | Plugins are standalone executables or source files that are loaded by `wi`. `wi` 59 | discovers plugins on startup by looking for `wi-plugin-*` / `wi-plugin-*.exe` 60 | and `wi-plugin-*.go` in the same directory (`$GOPATH/bin`) as the `wi` 61 | executable. 62 | 63 | ``` 64 | go get -u github.com/someone/wi-plugin-awesome 65 | ``` 66 | 67 | `*.go` files are sent to `go run` for _on-the-fly_ compilation at 68 | the cost of slower startup time so there's no native updating support. 69 | 70 | 71 | Vision 72 | ------ 73 | 74 | - No I/O done in the UI thread. UI must always be responsive even on a I/O 75 | saturated system. 76 | - Very extensible editor _with sane default settings_. 77 | - _Historical reasons are not good reasons_. 78 | - Out of process plugins. If a 79 | [web browser](http://dev.chromium.org/developers/design-documents/multi-process-architecture) 80 | can render web pages out of process, an editor can do the same. 81 | - Plugins written in the same language as the editor itself. No need to learn 82 | yet another language (vimscript? lisp? python? javascript?). 83 | - *The only text editor with statically compiled macros!* 84 | - Use instrinsic Go distribution mechanism to distribute plugins. Stable 85 | release is `go1` branch. 86 | - Broad OS support, including seamless Windows support. 87 | - Unicode used internally. 88 | 89 | 90 | Contributing 91 | ------------ 92 | 93 | First make sure test-only dependencies are installed with the flag `-t` then 94 | fetch and install `pre-commit-go`: 95 | 96 | cd github.com//wi 97 | go get -t ./... 98 | go get github.com/maruel/pre-commit-go 99 | pre-commit-go 100 | 101 | Once done, you can send pull requests. 102 | 103 | A CLA (_form to be determined_) may eventually be required for contribution. 104 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Use "go build -tags debug" to have access to the code and commands in this 6 | // file. 7 | 8 | // +build debug 9 | 10 | package main 11 | 12 | import ( 13 | "encoding/base64" 14 | "encoding/json" 15 | "expvar" 16 | "flag" 17 | "fmt" 18 | "html/template" 19 | "io" 20 | "log" 21 | "net/http" 22 | _ "net/http/pprof" 23 | "os" 24 | "runtime/pprof" 25 | "sort" 26 | "strings" 27 | 28 | "github.com/maruel/circular" 29 | "github.com/wi-ed/wi/editor" 30 | "github.com/wi-ed/wi/wicore" 31 | "github.com/wi-ed/wi/wicore/key" 32 | "github.com/wi-ed/wi/wicore/lang" 33 | ) 34 | 35 | var ( 36 | httpServer = flag.String("http", "", "Start a debug web server to observe internal states") 37 | cpuprofile = flag.String("cpuprofile", "", "Write cpu profile to file; use \"go tool pprof wi \" to read the data; See https://blog.golang.org/profiling-go-programs for more details") 38 | data debugData 39 | ) 40 | 41 | type debugData struct { 42 | logBuffer circular.Buffer 43 | logFile io.Closer 44 | profFile io.Closer 45 | } 46 | 47 | func (d *debugData) Close() error { 48 | if d.profFile != nil { 49 | pprof.StopCPUProfile() 50 | d.profFile.Close() 51 | d.profFile = nil 52 | } 53 | log.Printf("Closing log") 54 | d.logBuffer.Flush() 55 | d.logBuffer.Close() 56 | if d.logFile != nil { 57 | d.logFile.Close() 58 | d.logFile = nil 59 | } 60 | return nil 61 | } 62 | 63 | func debugHook() io.Closer { 64 | log.SetFlags(log.Lmicroseconds | log.Lshortfile) 65 | data.logBuffer = circular.New(10 * 1024 * 1024) 66 | log.SetOutput(data.logBuffer) 67 | if f, err := os.OpenFile("wi.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666); err == nil { 68 | wicore.Go("Log flusher", func() { data.logBuffer.WriteTo(f) }) 69 | } 70 | 71 | if *cpuprofile != "" { 72 | if f, err := os.OpenFile(*cpuprofile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666); err == nil { 73 | data.profFile = f 74 | pprof.StartCPUProfile(f) 75 | } else { 76 | log.Printf("Failed to open %s: %s", *cpuprofile, err) 77 | *cpuprofile = "" 78 | } 79 | } 80 | 81 | // TODO(maruel): Investigate adding our own profiling for RPC. 82 | // http://golang.org/pkg/runtime/pprof/ 83 | // TODO(maruel): Add pprof.WriteHeapProfile(f) when desired (?) 84 | 85 | if *httpServer != "" { 86 | http.HandleFunc("/", rootHandler) 87 | http.HandleFunc("/favicon.ico", faviconHandler) 88 | http.HandleFunc("/log", logHandler) 89 | wicore.Go("HTTPserver", func() { 90 | log.Println(http.ListenAndServe(*httpServer, nil)) 91 | }) 92 | } 93 | return &data 94 | } 95 | 96 | func debugHookEditor(e editor.Editor) { 97 | expvar.Publish("active_window", funcString(func() string { return e.ActiveWindow().String() })) 98 | expvar.Publish("commands", funcJSON(func() interface{} { return commands(e) })) 99 | expvar.Publish("documents", funcJSON(func() interface{} { return documents(e) })) 100 | expvar.Publish("view_factories", funcJSON(func() interface{} { return viewFactories(e) })) 101 | expvar.Publish("windows", funcJSON(func() interface{} { return windows(e) })) 102 | expvar.NewInt("pid").Set(int64(os.Getpid())) 103 | 104 | cmds := []wicore.Command{ 105 | &wicore.CommandImpl{ 106 | "command_log", 107 | 0, 108 | cmdCommandLog, 109 | wicore.DebugCategory, 110 | lang.Map{ 111 | lang.En: "Logs the registered commands", 112 | }, 113 | lang.Map{ 114 | lang.En: "Logs the registered commands, this is only relevant if -verbose is used.", 115 | }, 116 | }, 117 | &wicore.CommandImpl{ 118 | "key_log", 119 | 0, 120 | cmdKeyLog, 121 | wicore.DebugCategory, 122 | lang.Map{ 123 | lang.En: "Logs the key bindings", 124 | }, 125 | lang.Map{ 126 | lang.En: "Logs the key bindings, this is only relevant if -verbose is used.", 127 | }, 128 | }, 129 | &wicore.CommandImpl{ 130 | "log_all", 131 | 0, 132 | cmdLogAll, 133 | wicore.DebugCategory, 134 | lang.Map{ 135 | lang.En: "Logs the internal state (commands, view factories, windows)", 136 | }, 137 | lang.Map{ 138 | lang.En: "Logs the internal state (commands, view factories, windows), this is only relevant if -verbose is used.", 139 | }, 140 | }, 141 | &wicore.CommandImpl{ 142 | "view_log", 143 | 0, 144 | cmdViewLog, 145 | wicore.DebugCategory, 146 | lang.Map{ 147 | lang.En: "Logs the view factories", 148 | }, 149 | lang.Map{ 150 | lang.En: "Logs the view factories, this is only relevant if -verbose is used.", 151 | }, 152 | }, 153 | &wicore.CommandImpl{ 154 | "window_log", 155 | 0, 156 | cmdWindowLog, 157 | wicore.DebugCategory, 158 | lang.Map{ 159 | lang.En: "Logs the window tree", 160 | }, 161 | lang.Map{ 162 | lang.En: "Logs the window tree, this is only relevant if -verbose is used.", 163 | }, 164 | }, 165 | 166 | // 'editor_screenshot', mainly for unit test; open a new buffer with the screenshot, so it can be saved with 'w'. 167 | } 168 | // TODO(maruel): Handle out of process view. 169 | viewW, ok := wicore.RootWindow(e.ActiveWindow()).View().(wicore.ViewW) 170 | if !ok { 171 | panic("internal error") 172 | } 173 | dispatcher := viewW.CommandsW() 174 | for _, cmd := range cmds { 175 | dispatcher.Register(cmd) 176 | } 177 | } 178 | 179 | // prettyPrintJSON pretty-prints a JSON buffer. Accepts list and dict. 180 | func prettyPrintJSON(in []byte) []byte { 181 | var data interface{} 182 | var asMap map[string]interface{} 183 | if err := json.Unmarshal(in, &asMap); err != nil { 184 | var asList []interface{} 185 | if err := json.Unmarshal(in, &asList); err != nil { 186 | data = err.Error() 187 | } else { 188 | data = asList 189 | } 190 | } else { 191 | data = asMap 192 | } 193 | out, err := json.MarshalIndent(data, "", " ") 194 | if err != nil { 195 | return []byte(err.Error()) 196 | } 197 | return out 198 | } 199 | 200 | var tmplRoot = template.Must(template.New("root").Parse(` 201 | 202 | 203 | wi internals 204 | 205 | 222 | 223 | 224 |

wi internal details

225 | 238 |
239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | {{range .Values}} 248 | 249 | 250 | 251 | 252 | {{end}} 253 | 254 |
NameValue
{{index . 0}}
{{index . 1}}
255 | 256 | `)) 257 | 258 | func rootHandler(w http.ResponseWriter, r *http.Request) { 259 | if r.URL.Path != "/" { 260 | http.Redirect(w, r, "/", http.StatusMovedPermanently) 261 | return 262 | } 263 | d := struct { 264 | Values [][2]string 265 | }{ 266 | [][2]string{}, 267 | } 268 | expvar.Do(func(kv expvar.KeyValue) { 269 | v := kv.Value.String() 270 | if _, ok := kv.Value.(expvar.Func); ok { 271 | v = string(prettyPrintJSON([]byte(v))) 272 | } 273 | d.Values = append(d.Values, [2]string{kv.Key, v}) 274 | }) 275 | if err := tmplRoot.Execute(w, d); err != nil { 276 | io.WriteString(w, err.Error()) 277 | } 278 | } 279 | 280 | func logHandler(w http.ResponseWriter, r *http.Request) { 281 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 282 | data.logBuffer.WriteTo(w) 283 | } 284 | 285 | func faviconHandler(w http.ResponseWriter, r *http.Request) { 286 | w.Header().Set("Content-Type", "image/x-image") 287 | wiPNG, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAVUlEQVQ4y2NgGGjACGP8////PzkGMFHqAhQDrklLY1WELI5LDcN/KLgqJfUfGaDz0QHJXkB3AROxCkkKxGvS0gxaT58SZQiGAVpPn+LlD/J0MEINAAC5TUkhJn+lswAAAABJRU5ErkJggg==") 288 | w.Write(wiPNG) 289 | } 290 | 291 | type funcString func() string 292 | 293 | func (f funcString) String() string { 294 | return f() 295 | } 296 | 297 | type funcJSON func() interface{} 298 | 299 | func (f funcJSON) String() string { 300 | v, _ := json.MarshalIndent(f(), "", " ") 301 | return string(v) 302 | } 303 | 304 | func commandRecurse(w wicore.Window, buf []string) []string { 305 | cmds := w.View().Commands() 306 | for _, name := range cmds.GetNames() { 307 | c := cmds.Get(name) 308 | buf = append(buf, fmt.Sprintf("%-3s %-21s: %s", w.ID(), c.Name(), c.ShortDesc())) 309 | } 310 | for _, child := range w.ChildrenWindows() { 311 | buf = commandRecurse(child, buf) 312 | } 313 | return buf 314 | } 315 | 316 | func commands(e wicore.Editor) interface{} { 317 | // Start at the root and recurse. 318 | out := commandRecurse(wicore.RootWindow(e.ActiveWindow()), []string{}) 319 | sort.Strings(out) 320 | return out 321 | } 322 | 323 | func documents(e wicore.Editor) interface{} { 324 | return e.AllDocuments() 325 | } 326 | 327 | func viewFactories(e wicore.Editor) interface{} { 328 | names := e.ViewFactoryNames() 329 | sort.Strings(names) 330 | return names 331 | } 332 | 333 | func recurseTree(w wicore.Window) map[string]interface{} { 334 | out := map[string]interface{}{ 335 | "rect": w.Rect(), 336 | "title": w.View().Title(), 337 | "id": w.ID(), 338 | } 339 | children := []interface{}{} 340 | for _, child := range w.ChildrenWindows() { 341 | children = append(children, recurseTree(child)) 342 | } 343 | // Use z_ so it's the last item, for easier browsing. 344 | if len(children) != 0 { 345 | out["z_children"] = children 346 | } 347 | return out 348 | } 349 | 350 | func windows(e wicore.Editor) interface{} { 351 | return recurseTree(wicore.RootWindow(e.ActiveWindow())) 352 | } 353 | 354 | func cmdCommandLog(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 355 | out := commandRecurse(wicore.RootWindow(e.ActiveWindow()), []string{}) 356 | sort.Strings(out) 357 | for _, i := range out { 358 | log.Printf(" %s", i) 359 | } 360 | } 361 | 362 | func keyLogRecurse(w wicore.Window, e wicore.EditorW, mode wicore.KeyboardMode) { 363 | bindings := w.View().KeyBindings() 364 | assigned := bindings.GetAssigned(mode) 365 | names := make([]string, 0, len(assigned)) 366 | for _, k := range assigned { 367 | names = append(names, k.String()) 368 | } 369 | sort.Strings(names) 370 | for _, name := range names { 371 | log.Printf(" %s %s: %s", w.ID(), name, bindings.Get(mode, key.StringToPress(name))) 372 | } 373 | for _, child := range w.ChildrenWindows() { 374 | keyLogRecurse(child, e, mode) 375 | } 376 | } 377 | 378 | func cmdKeyLog(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 379 | log.Printf("Normal commands") 380 | rootWindow := wicore.RootWindow(e.ActiveWindow()) 381 | keyLogRecurse(rootWindow, e, wicore.Normal) 382 | log.Printf("Insert commands") 383 | keyLogRecurse(rootWindow, e, wicore.Insert) 384 | } 385 | 386 | func cmdLogAll(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 387 | e.ExecuteCommand(w, "command_log") 388 | e.ExecuteCommand(w, "window_log") 389 | e.ExecuteCommand(w, "view_log") 390 | e.ExecuteCommand(w, "key_log") 391 | } 392 | 393 | func cmdViewLog(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 394 | names := e.ViewFactoryNames() 395 | sort.Strings(names) 396 | log.Printf("View factories:") 397 | for _, name := range names { 398 | log.Printf(" %s", name) 399 | } 400 | } 401 | 402 | func tree(w wicore.Window) string { 403 | out := w.String() + "\n" 404 | for _, child := range w.ChildrenWindows() { 405 | for _, line := range strings.Split(tree(child), "\n") { 406 | if line != "" { 407 | out += (" " + line + "\n") 408 | } 409 | } 410 | } 411 | return out 412 | } 413 | 414 | func cmdWindowLog(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 415 | root := wicore.RootWindow(w) 416 | log.Printf("Window tree:\n%s", tree(root)) 417 | } 418 | -------------------------------------------------------------------------------- /editor/command_view.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "github.com/wi-ed/wi/wicore" 9 | "github.com/wi-ed/wi/wicore/colors" 10 | "github.com/wi-ed/wi/wicore/key" 11 | "github.com/wi-ed/wi/wicore/raster" 12 | ) 13 | 14 | // commandView would normally be in a floating Window near the current cursor 15 | // on the last focused Window or at the very last line at the bottom of the 16 | // screen. 17 | type commandView struct { 18 | view 19 | text string 20 | } 21 | 22 | func (v *commandView) Buffer() *raster.Buffer { 23 | v.buffer.Fill(raster.Cell{' ', v.DefaultFormat()}) 24 | v.buffer.DrawString(v.text, 0, 0, v.DefaultFormat()) 25 | return v.buffer 26 | } 27 | 28 | func (v *commandView) onTerminalKeyPressed(k key.Press) { 29 | // TODO(maruel): React to keys. 30 | if k.Ch != '\000' { 31 | v.text += string(k.Ch) 32 | } else { 33 | switch k.Key { 34 | case key.Escape: 35 | // Dismiss window. 36 | case key.Enter: 37 | // Execute command. 38 | case key.Space: 39 | v.text += " " 40 | case key.Tab: 41 | // Command completion. 42 | } 43 | } 44 | } 45 | 46 | // The command dialog box. 47 | // 48 | // TODO(maruel): Position it 5 lines below the cursor in the parent Window's 49 | // View. Do this via onAttach. 50 | func commandViewFactory(e wicore.Editor, id int, args ...string) wicore.ViewW { 51 | bindings := makeKeyBindings() 52 | // Fill up the key bindings. This includes basic cursor movement, help, etc. 53 | //bindings.Set(wicore.AllMode, key.Press{Key: key.Enter}, "execute_command") 54 | //bindings.Set(wicore.AllMode, key.Press{Key: key.Escape}, "window_close") 55 | v := &commandView{ 56 | view{ 57 | commands: makeCommands(), 58 | keyBindings: bindings, 59 | id: id, 60 | title: "Command", 61 | naturalX: 30, 62 | naturalY: 1, 63 | defaultFormat: raster.CellFormat{Fg: colors.Green, Bg: colors.Black}, 64 | }, 65 | "", 66 | } 67 | event := e.RegisterTerminalKeyPressed(v.onTerminalKeyPressed) 68 | e.RegisterViewActivated(func(v wicore.View) { 69 | _ = event.Close() 70 | }) 71 | return v 72 | } 73 | -------------------------------------------------------------------------------- /editor/commands.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "github.com/wi-ed/wi/wicore" 9 | "github.com/wi-ed/wi/wicore/lang" 10 | ) 11 | 12 | // commands is the map of registered commands. 13 | type commands struct { 14 | commands map[string]wicore.Command 15 | names []string 16 | } 17 | 18 | func (c *commands) Register(cmd wicore.Command) bool { 19 | name := cmd.Name() 20 | _, ok := c.commands[name] 21 | c.commands[name] = cmd 22 | c.names = nil 23 | return !ok 24 | } 25 | 26 | func (c *commands) Get(cmd string) wicore.Command { 27 | return c.commands[cmd] 28 | } 29 | 30 | func (c *commands) GetNames() []string { 31 | if c.names == nil { 32 | c.names = make([]string, 0, len(c.commands)) 33 | for name := range c.commands { 34 | c.names = append(c.names, name) 35 | } 36 | } 37 | return c.names 38 | } 39 | 40 | func makeCommands() wicore.CommandsW { 41 | return &commands{make(map[string]wicore.Command), nil} 42 | } 43 | 44 | // privilegedCommandImplHandler is the CommandHandler to use when coupled with 45 | // privilegedCommandImpl. 46 | type privilegedCommandImplHandler func(c *privilegedCommandImpl, e *editor, w *window, args ...string) 47 | 48 | // privilegedCommandImpl is the boilerplate Command implementation for builtin 49 | // commands that can access the editor directly. 50 | // 51 | // Native (builtin) commands can mutate the editor. 52 | // 53 | // This command handler has access to the internals of the editor. Because of 54 | // this, it can only be native commands inside the editor process. 55 | type privilegedCommandImpl struct { 56 | NameValue string 57 | ExpectedArgs int // If >= 0, the command will be aborted if the number of arguments is not exactly this value. Set to -1 to disable verification. On abort, an alert with the long description of the command is done. 58 | HandlerValue privilegedCommandImplHandler 59 | CategoryValue wicore.CommandCategory 60 | ShortDescValue lang.Map 61 | LongDescValue lang.Map 62 | } 63 | 64 | func (c *privilegedCommandImpl) Name() string { 65 | return c.NameValue 66 | } 67 | 68 | func (c *privilegedCommandImpl) Handle(e wicore.EditorW, w wicore.Window, args ...string) { 69 | if c.ExpectedArgs != -1 && len(args) != c.ExpectedArgs { 70 | e.ExecuteCommand(w, "alert", c.LongDesc()) 71 | } 72 | // Convert types to internal types. 73 | ed := e.(*editor) 74 | wInternal := w.(*window) 75 | c.HandlerValue(c, ed, wInternal, args...) 76 | } 77 | 78 | func (c *privilegedCommandImpl) Category(e wicore.Editor, w wicore.Window) wicore.CommandCategory { 79 | return c.CategoryValue 80 | } 81 | 82 | func (c *privilegedCommandImpl) ShortDesc() string { 83 | return c.ShortDescValue.String() 84 | } 85 | 86 | func (c *privilegedCommandImpl) LongDesc() string { 87 | return c.LongDescValue.String() 88 | } 89 | 90 | // Commands 91 | 92 | func cmdCommandAlias(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 93 | if args[0] == "window" { 94 | } else if args[0] == "global" { 95 | w = wicore.RootWindow(w) 96 | } else { 97 | cmd := wicore.GetCommand(e, w, "command_alias") 98 | e.ExecuteCommand(w, "alert", cmd.LongDesc()) 99 | return 100 | } 101 | alias := &wicore.CommandAlias{args[1], args[2], nil} 102 | // TODO(maruel): Handle views in different process? 103 | viewW, ok := w.View().(wicore.ViewW) 104 | if !ok { 105 | e.ExecuteCommand(w, "alert", "internal failure") 106 | return 107 | } 108 | viewW.CommandsW().Register(alias) 109 | } 110 | 111 | // RegisterCommandCommands registers the top-level native commands. 112 | func RegisterCommandCommands(dispatcher wicore.CommandsW) { 113 | cmds := []wicore.Command{ 114 | &wicore.CommandImpl{ 115 | "command_alias", 116 | 3, 117 | cmdCommandAlias, 118 | wicore.CommandsCategory, 119 | lang.Map{ 120 | lang.En: "Binds an alias to another command", 121 | }, 122 | lang.Map{ 123 | // TODO(maruel): For complex aliasing, use macro? 124 | lang.En: "Usage: command_alias [window|global] \nBinds an alias to another command. The alias can either be local to the window or global", 125 | }, 126 | }, 127 | 128 | &wicore.CommandAlias{"alias", "command_alias", nil}, 129 | } 130 | for _, cmd := range cmds { 131 | dispatcher.Register(cmd) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /editor/commands_todo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "log" 9 | 10 | "github.com/wi-ed/wi/wicore" 11 | "github.com/wi-ed/wi/wicore/lang" 12 | ) 13 | 14 | func cmdDoc(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 15 | // TODO(maruel): Grab the current word under selection if no args is 16 | // provided. Pass this token to shell. 17 | docArgs := make([]string, len(args)+1) 18 | docArgs[0] = "doc" 19 | copy(docArgs[1:], args) 20 | //dispatcher.Execute(w, "shell", docArgs...) 21 | } 22 | 23 | func cmdHelp(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 24 | // TODO(maruel): Creates a new Window with a ViewHelp. 25 | log.Printf("Faking help: %s", args) 26 | } 27 | 28 | func cmdShell(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 29 | log.Printf("Faking opening a new shell: %s", args) 30 | } 31 | 32 | // RegisterTodoCommands registers the top-level native commands that are yet to 33 | // be implemented. 34 | // 35 | // TODO(maruel): Implement these commands properly and move to the right place. 36 | func RegisterTodoCommands(dispatcher wicore.CommandsW) { 37 | cmds := []wicore.Command{ 38 | &wicore.CommandImpl{ 39 | "doc", 40 | -1, 41 | cmdDoc, 42 | wicore.WindowCategory, 43 | lang.Map{ 44 | lang.En: "Search godoc documentation", 45 | }, 46 | lang.Map{ 47 | lang.En: "Uses the 'doc' tool to get documentation about the text under the cursor.", 48 | }, 49 | }, 50 | &wicore.CommandImpl{ 51 | "help", 52 | -1, 53 | cmdHelp, 54 | wicore.WindowCategory, 55 | lang.Map{ 56 | lang.En: "Prints help", 57 | }, 58 | lang.Map{ 59 | lang.En: "Prints general help or help for a particular command.", 60 | }, 61 | }, 62 | &wicore.CommandImpl{ 63 | "shell", 64 | -1, 65 | cmdShell, 66 | wicore.WindowCategory, 67 | lang.Map{ 68 | lang.En: "Opens a shell process", 69 | }, 70 | lang.Map{ 71 | lang.En: "Opens a shell process in a new buffer.", 72 | }, 73 | }, 74 | } 75 | for _, cmd := range cmds { 76 | dispatcher.Register(cmd) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /editor/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Package editor contains the UI toolkit agnostic unit-testable part of the wi 6 | // editor. It brings text based editor technology past 1200 bauds. 7 | // 8 | // It is in a standalone package for a few reasons: 9 | // - godoc will generate documentation for this code. 10 | // - it can be unit tested without having a dependency on termbox. 11 | // - hide away ncurse idiocracies (like Ctrl-H == Backspace) which could be 12 | // supported on Windows or native UI. 13 | // 14 | // This package is not meant to be a general purpose reusable package, the 15 | // primary maintainer of this project likes having a web browseable 16 | // documentation. Using a package is an effective workaround the fact that 17 | // godoc doesn't general documentation for "main" package. 18 | // 19 | // See ../README.md for user information. 20 | package editor 21 | -------------------------------------------------------------------------------- /editor/document.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "log" 11 | "strings" 12 | "unicode" 13 | 14 | "github.com/wi-ed/wi/wicore" 15 | "github.com/wi-ed/wi/wicore/lang" 16 | "github.com/wi-ed/wi/wicore/raster" 17 | ) 18 | 19 | // ReadWriteSeekCloser is a generic handle to a file. 20 | // 21 | // TODO(maruel): No idea why package io doesn't provide this interface. 22 | type ReadWriteSeekCloser interface { 23 | io.Closer 24 | io.ReadWriteSeeker 25 | } 26 | 27 | // document is a live editable document. 28 | // 29 | // TODO(maruel): This will probably have to be moved into wicore, since 30 | // documents could be useful to plugins (?) 31 | // 32 | // TODO(maruel): editor needs to have the list of opened document. They may 33 | // have multiple views associated to a single document. 34 | // 35 | // TODO(maruel): Strictly speaking, a Window could be in the wi parent process, 36 | // a View in a plugin process and a Document in a separate plugin process (e.g. 37 | // output from a live command, whatever). This means wicore.Document would need 38 | // to be a proper interface. 39 | type document struct { 40 | filePath string // filePath encoded in unicode. This can cause problems with systems not using an unicode code page. 41 | fileType string // One of the known file type. Generally described by a file extension, optionally followed by a version (?). TODO(maruel): Design. 42 | handle ReadWriteSeekCloser // Handle to the file. For unsaved files, it's empty. 43 | content []string // Content as a slice of string, each being a line. In practice, it could be desired that a document not to be fully loaded in memory, or loaded asynchronously. TODO(maruel): Implement partial loading. 44 | isDirty bool // true if the content was not saved to disk. 45 | } 46 | 47 | func makeDocument() *document { 48 | return &document{ 49 | // TODO(maruel): Obviously, no initial content. 50 | content: []string{"Dummy content\n", "Really\n"}, 51 | } 52 | } 53 | 54 | func (d *document) ID() string { 55 | // TODO(maruel): This implies the same document should never be loaded twice. 56 | // It think it's a valid assumption, multiple DocumentView should be created 57 | // instead. 58 | // TODO(maruel): Implement uniqueness. And look at hardlinks and symlinks too 59 | // to dedupe paths. 60 | return fmt.Sprintf("document:%s", d.filePath) 61 | } 62 | 63 | func (d *document) String() string { 64 | return fmt.Sprintf("Document(%s)", d.filePath) 65 | } 66 | 67 | func (d *document) Close() error { 68 | return nil 69 | } 70 | 71 | func (d *document) RenderInto(buffer *raster.Buffer, view wicore.View, offsetColumn, offsetLine int) { 72 | for row, l := range d.content { 73 | // This will automatically elide text. 74 | if offsetColumn != 0 { 75 | // TODO(maruel): This is a hot path and should be optimized accordingly 76 | // by not requiring converting the full string. 77 | // TODO(maruel): Handle zero width space U+200B. It should (obviously) 78 | // not take any space. 79 | l = string([]rune(l)[offsetColumn:]) 80 | } 81 | // It is particularly important on Windows, as "\n" would be rendered as an invalid character. 82 | l := strings.TrimRightFunc(l, unicode.IsSpace) 83 | buffer.DrawString(l, offsetColumn, row+offsetLine, view.DefaultFormat()) 84 | } 85 | } 86 | 87 | func (d *document) FileType() wicore.FileType { 88 | return wicore.Scanning 89 | } 90 | 91 | func (d *document) IsDirty() bool { 92 | return d.isDirty 93 | } 94 | 95 | // Commands. 96 | 97 | func cmdDocumentBuild(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 98 | e.ExecuteCommand(w, "alert", "Implement 'document_build' for your document") 99 | } 100 | 101 | func cmdDocumentNew(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 102 | cmd := make([]string, 3+len(args)) 103 | //cmd[0] = w.ID() 104 | cmd[0] = wicore.RootWindow(w).ID() 105 | cmd[1] = "fill" 106 | cmd[2] = "new_document" 107 | copy(cmd[3:], args) 108 | e.ExecuteCommand(w, "window_new", cmd...) 109 | } 110 | 111 | func cmdDocumentOpen(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 112 | // The Window and View are created synchronously. The View is populated 113 | // asynchronously. 114 | log.Printf("Faking opening a file: %s", args) 115 | } 116 | 117 | func cmdDocumentRun(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 118 | e.ExecuteCommand(w, "alert", "Implement 'document_run' for your document") 119 | } 120 | 121 | // RegisterDocumentCommands registers the top-level native commands to manage 122 | // documents. 123 | func RegisterDocumentCommands(dispatcher wicore.CommandsW) { 124 | cmds := []wicore.Command{ 125 | &wicore.CommandImpl{ 126 | "document_build", 127 | 0, 128 | cmdDocumentBuild, 129 | wicore.WindowCategory, 130 | lang.Map{ 131 | lang.En: "Build a file", 132 | }, 133 | lang.Map{ 134 | lang.En: "Build a file.", 135 | }, 136 | }, 137 | &wicore.CommandImpl{ 138 | "document_new", 139 | 0, 140 | cmdDocumentNew, 141 | wicore.WindowCategory, 142 | lang.Map{ 143 | lang.En: "Create a new buffer", 144 | }, 145 | lang.Map{ 146 | // TODO(maruel): Add a command to create a new buffer without a new 147 | // window. Wrapper command does the connection to create doc, open new 148 | // window, then load buffer into window. 149 | lang.En: "Create a new buffer. It also creates a new window to hold the document.", 150 | }, 151 | }, 152 | &wicore.CommandImpl{ 153 | "document_open", 154 | 1, 155 | cmdDocumentOpen, 156 | wicore.WindowCategory, 157 | lang.Map{ 158 | lang.En: "Opens a file in a new buffer", 159 | }, 160 | lang.Map{ 161 | lang.En: "Opens a file in a new buffer.", 162 | }, 163 | }, 164 | &wicore.CommandImpl{ 165 | "document_run", 166 | 0, 167 | cmdDocumentRun, 168 | wicore.WindowCategory, 169 | lang.Map{ 170 | lang.En: "Run a file", 171 | }, 172 | lang.Map{ 173 | lang.En: "Run a file.", 174 | }, 175 | }, 176 | 177 | &wicore.CommandAlias{"new", "document_new", nil}, 178 | &wicore.CommandAlias{"o", "document_open", nil}, 179 | &wicore.CommandAlias{"open", "document_open", nil}, 180 | } 181 | for _, cmd := range cmds { 182 | dispatcher.Register(cmd) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /editor/document_view.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "github.com/wi-ed/wi/wicore" 9 | "github.com/wi-ed/wi/wicore/colors" 10 | "github.com/wi-ed/wi/wicore/key" 11 | "github.com/wi-ed/wi/wicore/lang" 12 | "github.com/wi-ed/wi/wicore/raster" 13 | ) 14 | 15 | // ColorMode is the coloring mode in effect. 16 | // 17 | // TODO(maruel): Define coloring modes. Could be: 18 | // - A file type. Likely defined by a string, not a int. 19 | // - A diff view mode. 20 | // - No color at all. 21 | type ColorMode int 22 | 23 | // documentView is the View of a Document. There can be multiple views of the 24 | // same document, each with their own cursor position. 25 | // 26 | // TODO(maruel): In some cases, the cursor position could be shared. A good 27 | // example is vimdiff in 4-way mode. 28 | // 29 | // TODO(maruel): The part that is serializable has to be in its own structure 30 | // for easier deserialization. 31 | type documentView struct { 32 | view 33 | document *document 34 | cursorLine int // cursor position is 0-based. 35 | cursorColumn int 36 | cursorColumnMax int // cursor position if the line was long enough. 37 | offsetLine int // Offset of the view of the document. 38 | offsetColumn int // Offset of the view of the document. Only make sense when wordWrap==false. 39 | wordWrap bool // true if word-wrapping is in effect. TODO(maruel): Implement. 40 | columnMode bool // true if free movement is in effect. TODO(maruel): Implement. 41 | colorMode ColorMode // Coloring of the file. Technically it'd be possible to have one file view without color and another with. TODO(maruel): Determine if useful. 42 | selection raster.Rect // selection if any. TODO(maruel): Selection in columnMode vs normal selection vs line selection. 43 | } 44 | 45 | func (v *documentView) Close() error { 46 | err := v.view.Close() 47 | err2 := v.document.Close() 48 | if err != nil { 49 | return err 50 | } 51 | return err2 52 | } 53 | 54 | func (v *documentView) Buffer() *raster.Buffer { 55 | v.buffer.Fill(raster.Cell{' ', v.defaultFormat}) 56 | v.document.RenderInto(v.buffer, v, v.offsetColumn, v.offsetLine) 57 | // TODO(maruel): Draw the cursor using proper terminal function. 58 | cell := v.buffer.Cell(v.offsetColumn+v.cursorColumn, v.offsetLine+v.cursorLine) 59 | cell.F.Bg = colors.White 60 | cell.F.Fg = colors.Black 61 | // TODO(maruel): Draw the selection over. 62 | return v.buffer 63 | } 64 | 65 | // cursorMoved triggers the event and ensures the cursor is visible. 66 | func (v *documentView) cursorMoved(e wicore.Editor) { 67 | e.TriggerDocumentCursorMoved(v.document, v.cursorColumn, v.cursorLine) 68 | // TODO(maruel): Adjust v.offsetLine and v.offsetColumn as necessary. 69 | // TODO(maruel): Trigger redraw. 70 | } 71 | 72 | func (v *documentView) onKeyPress(e wicore.Editor, k key.Press) { 73 | // TODO(maruel): Only get when the View is active. 74 | l := v.document.content[v.cursorLine] 75 | v.document.content[v.cursorLine] = l[:v.cursorColumn] + string(k.Ch) + l[v.cursorColumn:] 76 | v.cursorColumn++ 77 | v.cursorColumnMax = v.cursorColumn 78 | v.cursorMoved(e) 79 | // TODO(maruel): Implement dirty instead. 80 | e.TriggerTerminalResized() 81 | } 82 | 83 | func cmdToDoc(handler func(v *documentView, e wicore.EditorW)) wicore.CommandImplHandler { 84 | return func(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 85 | v, ok := w.View().(*documentView) 86 | if !ok { 87 | e.ExecuteCommand(w, "alert", "Internal error") 88 | return 89 | } 90 | handler(v, e) 91 | } 92 | } 93 | 94 | func cmdDocumentCursorLeft(v *documentView, e wicore.EditorW) { 95 | if v.cursorColumn == 0 { 96 | // TODO(maruel): Make wrap behavior optional. 97 | if v.cursorLine == 0 { 98 | // TODO(maruel): Beep. 99 | return 100 | } 101 | v.cursorLine-- 102 | v.cursorColumn = len(v.document.content[v.cursorLine]) - 1 103 | } 104 | v.cursorColumnMax = v.cursorColumn 105 | v.cursorMoved(e) 106 | } 107 | 108 | func cmdDocumentCursorRight(v *documentView, e wicore.EditorW) { 109 | if v.cursorColumn == len(v.document.content[v.cursorLine])-1 { 110 | // TODO(maruel): Make wrap behavior optional. 111 | if v.cursorLine > len(v.document.content)-1 { 112 | // TODO(maruel): Beep. 113 | return 114 | } 115 | v.cursorLine++ 116 | v.cursorColumn = 0 117 | } else { 118 | v.cursorColumn++ 119 | } 120 | v.cursorColumnMax = v.cursorColumn 121 | v.cursorMoved(e) 122 | } 123 | 124 | func cmdDocumentCursorUp(v *documentView, e wicore.EditorW) { 125 | if v.cursorLine == 0 { 126 | // TODO(maruel): Beep. 127 | return 128 | } 129 | v.cursorLine-- 130 | if v.cursorColumn >= len(v.document.content[v.cursorLine]) { 131 | v.cursorColumn = len(v.document.content[v.cursorLine]) - 1 132 | } 133 | v.cursorMoved(e) 134 | } 135 | 136 | func cmdDocumentCursorDown(v *documentView, e wicore.EditorW) { 137 | if v.cursorLine >= len(v.document.content)-1 { 138 | // TODO(maruel): Beep. 139 | return 140 | } 141 | v.cursorLine++ 142 | if v.cursorColumn >= len(v.document.content[v.cursorLine]) { 143 | v.cursorColumn = len(v.document.content[v.cursorLine]) - 1 144 | } 145 | v.cursorMoved(e) 146 | } 147 | 148 | func cmdDocumentCursorHome(v *documentView, e wicore.EditorW) { 149 | if v.cursorLine != 0 || v.cursorColumnMax != 0 { 150 | v.cursorLine = 0 151 | v.cursorColumn = 0 152 | v.cursorColumnMax = v.cursorColumn 153 | v.cursorMoved(e) 154 | } 155 | } 156 | 157 | func cmdDocumentCursorEnd(v *documentView, e wicore.EditorW) { 158 | if v.cursorLine != len(v.document.content)-1 || v.cursorColumnMax != len(v.document.content[v.cursorLine])-1 { 159 | v.cursorLine = len(v.document.content) - 1 160 | v.cursorColumn = len(v.document.content[v.cursorLine]) - 1 161 | v.cursorColumnMax = v.cursorColumn 162 | v.cursorMoved(e) 163 | } 164 | } 165 | 166 | func documentViewFactory(e wicore.Editor, id int, args ...string) wicore.ViewW { 167 | dispatcher := makeCommands() 168 | cmds := []wicore.Command{ 169 | &wicore.CommandImpl{ 170 | "document_cursor_left", 171 | 0, 172 | cmdToDoc(cmdDocumentCursorLeft), 173 | wicore.WindowCategory, 174 | lang.Map{ 175 | lang.En: "Moves cursor left", 176 | }, 177 | lang.Map{ 178 | lang.En: "Moves cursor left.", 179 | }, 180 | }, 181 | &wicore.CommandImpl{ 182 | "document_cursor_right", 183 | 0, 184 | cmdToDoc(cmdDocumentCursorRight), 185 | wicore.WindowCategory, 186 | lang.Map{ 187 | lang.En: "Moves cursor right", 188 | }, 189 | lang.Map{ 190 | lang.En: "Moves cursor right.", 191 | }, 192 | }, 193 | &wicore.CommandImpl{ 194 | "document_cursor_up", 195 | 0, 196 | cmdToDoc(cmdDocumentCursorUp), 197 | wicore.WindowCategory, 198 | lang.Map{ 199 | lang.En: "Moves cursor up", 200 | }, 201 | lang.Map{ 202 | lang.En: "Moves cursor up.", 203 | }, 204 | }, 205 | &wicore.CommandImpl{ 206 | "document_cursor_down", 207 | 0, 208 | cmdToDoc(cmdDocumentCursorDown), 209 | wicore.WindowCategory, 210 | lang.Map{ 211 | lang.En: "Moves cursor down", 212 | }, 213 | lang.Map{ 214 | lang.En: "Moves cursor down.", 215 | }, 216 | }, 217 | &wicore.CommandImpl{ 218 | "document_cursor_home", 219 | 0, 220 | cmdToDoc(cmdDocumentCursorHome), 221 | wicore.WindowCategory, 222 | lang.Map{ 223 | lang.En: "Moves cursor to the beginning of the document", 224 | }, 225 | lang.Map{ 226 | lang.En: "Moves cursor to the beginning of the document.", 227 | }, 228 | }, 229 | &wicore.CommandImpl{ 230 | "document_cursor_end", 231 | 0, 232 | cmdToDoc(cmdDocumentCursorEnd), 233 | wicore.WindowCategory, 234 | lang.Map{ 235 | lang.En: "Moves cursor to the end of the document", 236 | }, 237 | lang.Map{ 238 | lang.En: "Moves cursor to the end of the document.", 239 | }, 240 | }, 241 | } 242 | for _, cmd := range cmds { 243 | dispatcher.Register(cmd) 244 | } 245 | 246 | bindings := makeKeyBindings() 247 | bindings.Set(wicore.AllMode, key.Press{Key: key.Left}, "document_cursor_left") 248 | bindings.Set(wicore.AllMode, key.Press{Key: key.Right}, "document_cursor_right") 249 | bindings.Set(wicore.AllMode, key.Press{Key: key.Up}, "document_cursor_up") 250 | bindings.Set(wicore.AllMode, key.Press{Key: key.Down}, "document_cursor_down") 251 | bindings.Set(wicore.AllMode, key.Press{Key: key.Home}, "document_cursor_home") 252 | bindings.Set(wicore.AllMode, key.Press{Key: key.End}, "document_cursor_end") 253 | // vim style movement. 254 | bindings.Set(wicore.Normal, key.Press{Ch: 'h'}, "document_cursor_left") 255 | bindings.Set(wicore.Normal, key.Press{Ch: 'l'}, "document_cursor_right") 256 | bindings.Set(wicore.Normal, key.Press{Ch: 'k'}, "document_cursor_up") 257 | bindings.Set(wicore.Normal, key.Press{Ch: 'j'}, "document_cursor_down") 258 | 259 | // TODO(maruel): Sort out "use max space". 260 | // TODO(maruel): Load last cursor position from config. 261 | v := &documentView{ 262 | view: view{ 263 | commands: dispatcher, 264 | keyBindings: bindings, 265 | id: id, 266 | title: "", // TODO(maruel): Title == document.filePath ? 267 | naturalX: 100, 268 | naturalY: 100, 269 | defaultFormat: raster.CellFormat{Fg: colors.BrightYellow, Bg: colors.Black}, 270 | }, 271 | document: makeDocument(), 272 | } 273 | v.onAttach = func(_ *view, w wicore.Window) { 274 | v.cursorMoved(e) 275 | } 276 | v.events = append(v.events, e.RegisterTerminalKeyPressed(func(k key.Press) { 277 | v.onKeyPress(e, k) 278 | })) 279 | return v 280 | } 281 | -------------------------------------------------------------------------------- /editor/editor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "io" 9 | "log" 10 | "time" 11 | 12 | "github.com/wi-ed/wi/wicore" 13 | "github.com/wi-ed/wi/wicore/key" 14 | "github.com/wi-ed/wi/wicore/lang" 15 | "github.com/wi-ed/wi/wicore/raster" 16 | ) 17 | 18 | const ( 19 | // Major.Minor.Bugfix. 20 | version = "0.0.1" 21 | ) 22 | 23 | // Editor is the inprocess wicore.Editor interface. It adds the process life-time 24 | // management functions to the public interface wicore.Editor. 25 | // 26 | // It is very important to call the Close() function upon termination. 27 | type Editor interface { 28 | io.Closer 29 | 30 | wicore.EditorW 31 | 32 | // EventLoop runs the event loop until the command "quit" executes 33 | // successfully. 34 | EventLoop() int 35 | } 36 | 37 | // editor is the global structure that holds everything together. It implements 38 | // the Editor interface. 39 | type editor struct { 40 | wicore.EventRegistry 41 | deferred chan func() 42 | terminal Terminal // Abstract terminal interface to the real terminal. 43 | rootWindow *window // The rootWindow is always DockingFill and set to the size of the terminal. 44 | lastActive []wicore.Window // Most recently used order of Window activatd. 45 | documents []wicore.Document // All loaded documents. 46 | viewFactories map[string]wicore.ViewFactory // All the ViewFactory's that can be used to create new View. 47 | viewReady chan bool // A View.Buffer() is ready to be drawn. 48 | keyboardMode wicore.KeyboardMode // Global keyboard mode instead of per Window, it's more logical for users. 49 | plugins Plugins // All loaded plugin processes. 50 | nextViewID int 51 | } 52 | 53 | func (e *editor) Close() error { 54 | if e.plugins == nil { 55 | return nil 56 | } 57 | err := e.plugins.Close() 58 | e.plugins = nil 59 | return err 60 | } 61 | 62 | func (e *editor) ID() string { 63 | // There shall be only one. 64 | return "editor" 65 | } 66 | 67 | func (e *editor) Version() string { 68 | return version 69 | } 70 | 71 | func (e *editor) onTerminalMetaKeyPressed(k key.Press) { 72 | if !k.IsValid() { 73 | panic("Unexpected non-key") 74 | } 75 | if !k.IsMeta() { 76 | panic("Unexpected non-meta") 77 | } 78 | cmdName := wicore.GetKeyBindingCommand(e, e.KeyboardMode(), k) 79 | if cmdName != "" { 80 | // The command is executed inline, since the key was already enqueued in 81 | // the event queue. 82 | e.ExecuteCommand(e.ActiveWindow(), cmdName) 83 | } else { 84 | e.ExecuteCommand(e.ActiveWindow(), "alert", notMapped.Formatf(k)) 85 | } 86 | } 87 | 88 | func (e *editor) onTerminalKeyPressed(k key.Press) { 89 | if !k.IsValid() { 90 | panic("Unexpected non-key") 91 | } 92 | if k.IsMeta() { 93 | panic("Unexpected meta") 94 | } 95 | } 96 | 97 | func (e *editor) ExecuteCommand(w wicore.Window, cmdName string, args ...string) { 98 | log.Printf("ExecuteCommand(%s, %s, %s)", w, cmdName, args) 99 | if w == nil { 100 | w = e.ActiveWindow() 101 | } 102 | cmd := wicore.GetCommand(e, w, cmdName) 103 | if cmd == nil { 104 | e.ExecuteCommand(w, "alert", notFound.Formatf(cmdName)) 105 | } else { 106 | cmd.Handle(e, w, args...) 107 | } 108 | } 109 | 110 | func (e *editor) onCommands(cmds wicore.EnqueuedCommands) { 111 | for _, cmd := range cmds.Commands { 112 | e.ExecuteCommand(e.ActiveWindow(), cmd[0], cmd[1:]...) 113 | } 114 | if cmds.Callback != nil { 115 | cmds.Callback() 116 | } 117 | } 118 | 119 | func (e *editor) KeyboardMode() wicore.KeyboardMode { 120 | return e.keyboardMode 121 | } 122 | 123 | // draw descends the whole Window tree and redraw Windows. 124 | func (e *editor) draw() { 125 | log.Print("draw()") 126 | // TODO(maruel): Cache the buffer. 127 | w, h := e.terminal.Size() 128 | out := raster.NewBuffer(w, h) 129 | drawRecurse(e.rootWindow, 0, 0, out) 130 | e.terminal.Blit(out) 131 | } 132 | 133 | func (e *editor) AllDocuments() []wicore.Document { 134 | out := make([]wicore.Document, len(e.documents)) 135 | for i, v := range e.documents { 136 | out[i] = v 137 | } 138 | return out 139 | } 140 | 141 | func (e *editor) AllPlugins() []wicore.PluginDetails { 142 | out := make([]wicore.PluginDetails, len(e.plugins)) 143 | for i, v := range e.plugins { 144 | out[i] = v.Details() 145 | } 146 | return out 147 | } 148 | 149 | func (e *editor) ActiveWindow() wicore.Window { 150 | return e.lastActive[0] 151 | } 152 | 153 | func (e *editor) activateWindow(w wicore.Window) { 154 | view := w.View() 155 | log.Printf("ActivateWindow(%s)", view.Title()) 156 | if view.IsDisabled() { 157 | e.ExecuteCommand(w, "alert", activateDisabled.String()) 158 | return 159 | } 160 | 161 | // First remove w from e.lastActive, second add w as e.lastActive[0]. 162 | // This kind of manual list shuffling is really Go's achille heel. 163 | // TODO(maruel): There's no way I got it right on the first try without a 164 | // unit test. 165 | for i, v := range e.lastActive { 166 | if v == w { 167 | if i > 0 { 168 | copy(e.lastActive[:i], e.lastActive[1:i+1]) 169 | e.lastActive[0] = w 170 | } 171 | return 172 | } 173 | } 174 | 175 | // This Window has never been active. 176 | l := len(e.lastActive) 177 | e.lastActive = append(e.lastActive, nil) 178 | copy(e.lastActive[:l], e.lastActive[1:l]) 179 | e.lastActive[0] = w 180 | e.TriggerViewActivated(view) 181 | } 182 | 183 | func (e *editor) RegisterViewFactory(name string, viewFactory wicore.ViewFactory) bool { 184 | _, present := e.viewFactories[name] 185 | e.viewFactories[name] = viewFactory 186 | return !present 187 | } 188 | 189 | func (e *editor) ViewFactoryNames() []string { 190 | names := make([]string, 0, len(e.viewFactories)) 191 | for name := range e.viewFactories { 192 | names = append(names, name) 193 | } 194 | return names 195 | } 196 | 197 | func (e *editor) onTerminalResized() { 198 | // Resize the Windows. This also invalidates it, which will also force a 199 | // redraw if the size changed. 200 | w, h := e.terminal.Size() 201 | e.rootWindow.setRect(raster.Rect{0, 0, w, h}) 202 | } 203 | 204 | func (e *editor) onDocumentCursorMoved(doc wicore.Document, col, line int) { 205 | // TODO(maruel): Obviously wrong. 206 | e.terminal.SetCursor(col, line) 207 | } 208 | 209 | func (e *editor) terminalLoop(terminal Terminal) { 210 | for event := range terminal.SeedEvents() { 211 | switch event.Type { 212 | case EventKey: 213 | if event.Key.IsValid() { 214 | if event.Key.IsMeta() { 215 | e.TriggerTerminalMetaKeyPressed(event.Key) 216 | } else { 217 | e.TriggerTerminalKeyPressed(event.Key) 218 | } 219 | } 220 | case EventResize: 221 | e.TriggerTerminalResized() 222 | } 223 | } 224 | } 225 | 226 | // EventLoop handles both commands and events from the editor. This function 227 | // runs in the UI goroutine. 228 | func (e *editor) EventLoop() int { 229 | fakeChan := make(chan time.Time) 230 | var drawTimer <-chan time.Time = fakeChan 231 | for { 232 | select { 233 | case i := <-e.deferred: 234 | if i == nil { 235 | // Happens on exit. Drawing only happens to make unit tests happy. 236 | // Should be removed eventually. 237 | e.draw() 238 | return 0 239 | } 240 | // The core of the event loop. See the generated file 241 | // event_registry_impl.go for how the functions are enqueued. 242 | i() 243 | 244 | case <-e.viewReady: 245 | // Taking in account a 60hz frame is 18.8ms, 5ms is going to be generally 246 | // processed within the same frame. This delaying results in significant 247 | // bandwidth saving on loading. 248 | if drawTimer == fakeChan { 249 | drawTimer = time.After(5 * time.Millisecond) 250 | } 251 | 252 | case <-drawTimer: 253 | // Empty e.viewReady first. 254 | EmptyViewReady: 255 | for { 256 | select { 257 | case <-e.viewReady: 258 | default: 259 | break EmptyViewReady 260 | } 261 | } 262 | 263 | e.draw() 264 | drawTimer = fakeChan 265 | } 266 | } 267 | } 268 | 269 | func (e *editor) isDirty() bool { 270 | for _, doc := range e.documents { 271 | if doc.IsDirty() { 272 | return true 273 | } 274 | } 275 | return false 276 | } 277 | 278 | func (e *editor) loadPlugins() { 279 | paths, err := enumPlugins(getPluginsPaths()) 280 | if err != nil { 281 | log.Printf("Failed to enum plugins: %s", err) 282 | } else { 283 | e.plugins, err = loadPlugins(paths) 284 | // Failing to load plugins is not a hard error. 285 | log.Printf("Loaded %d plugins", len(e.plugins)) 286 | if err != nil { 287 | log.Printf("Failed to load plugins: %s", err) 288 | } 289 | } 290 | // Trigger the RPC to initialize each plugin concurrently. Init() does not 291 | // wait for the plugin to be fully initialized. 292 | for _, plugin := range e.plugins { 293 | plugin.Init(e) 294 | } 295 | } 296 | 297 | // MakeEditor creates an object that implements the Editor interface. The root 298 | // window doesn't have anything to view in it. 299 | // 300 | // The editor contains a root window and a root view. It's up to the caller to 301 | // add child Windows in it. Normally it will be done via the command 302 | // "editor_bootstrap_ui" to add the status bar, then "new" or "open" to create 303 | // the initial text buffer. 304 | // 305 | // It is fine to run it concurrently in unit test, as no global variable shall 306 | // be used by the object created by this function. 307 | func MakeEditor(terminal Terminal, noPlugin bool) (Editor, error) { 308 | lang.Set(lang.En) 309 | reg, deferred := makeEventRegistry() 310 | e := &editor{ 311 | EventRegistry: reg, 312 | deferred: deferred, 313 | terminal: terminal, 314 | rootWindow: nil, // It is set below due to circular reference. 315 | lastActive: make([]wicore.Window, 1, 8), // It is set below. 316 | documents: []wicore.Document{}, 317 | viewFactories: make(map[string]wicore.ViewFactory), 318 | viewReady: make(chan bool), 319 | keyboardMode: wicore.Normal, 320 | nextViewID: 1, 321 | } 322 | 323 | // The root view is important, it defines all the global commands. It is 324 | // pre-filled with the default native commands and keyboard mapping, and it's 325 | // up to the plugins to add more global commands on startup. 326 | rootView := makeStaticDisabledView(e, 0, "Root", -1, -1) 327 | 328 | // These commands are generic commands, they do not require specific access. 329 | cmds := rootView.CommandsW() 330 | RegisterCommandCommands(cmds) 331 | RegisterKeyBindingCommands(cmds) 332 | RegisterViewCommands(cmds) 333 | RegisterWindowCommands(cmds) 334 | RegisterDocumentCommands(cmds) 335 | RegisterEditorDefaults(rootView) 336 | 337 | RegisterDefaultViewFactories(e) 338 | 339 | e.rootWindow = makeWindow(nil, rootView, wicore.DockingFill) 340 | e.rootWindow.e = e 341 | e.lastActive[0] = e.rootWindow 342 | 343 | e.RegisterTerminalMetaKeyPressed(e.onTerminalMetaKeyPressed) 344 | e.RegisterTerminalKeyPressed(e.onTerminalKeyPressed) 345 | e.RegisterTerminalResized(e.onTerminalResized) 346 | e.RegisterCommands(e.onCommands) 347 | e.RegisterDocumentCursorMoved(e.onDocumentCursorMoved) 348 | 349 | if !noPlugin { 350 | e.loadPlugins() 351 | } 352 | 353 | e.TriggerWindowCreated(e.rootWindow) 354 | e.TriggerViewCreated(rootView) 355 | // This forces creating the default buffer. 356 | e.TriggerTerminalResized() 357 | wicore.Go("terminalLoop", func() { e.terminalLoop(terminal) }) 358 | //e.TriggerEditorLanguage(lang.Active()) 359 | //e.TriggerEditorKeyboardModeChanged(e.keyboardMode) 360 | return e, nil 361 | } 362 | 363 | // Commands 364 | 365 | func cmdAlert(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 366 | e.ExecuteCommand(w, "window_new", "0", "bottom", "infobar_alert", args[0]) 367 | } 368 | 369 | func cmdEditorBootstrapUI(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 370 | e.ExecuteCommand(w, "window_new", "0", "bottom", "status_root") 371 | } 372 | 373 | func cmdEditorCommandWindow(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 374 | // Create the Window with the command view and attach it to the currently 375 | // focused Window. 376 | e.ExecuteCommand(w, "window_new", w.ID(), "floating", "command") 377 | } 378 | 379 | func cmdEditorQuit(c *privilegedCommandImpl, e *editor, w *window, args ...string) { 380 | if len(args) >= 1 { 381 | e.ExecuteCommand(w, "alert", c.LongDesc()) 382 | return 383 | } else if len(args) == 1 { 384 | if args[0] != "force" { 385 | e.ExecuteCommand(w, "alert", c.LongDesc()) 386 | return 387 | } 388 | } else { 389 | if e.isDirty() { 390 | // TODO(maruel): For each dirty Document, "prompt" y/n to force quit. If 391 | // 'n', stop there. 392 | return 393 | } 394 | // TODO(maruel): 395 | // - Send a signal to each plugin. 396 | // - Send a signal back to the main loop. 397 | } 398 | 399 | // This tells the editor.EventLoop() to quit. 400 | e.deferred <- nil 401 | } 402 | 403 | func cmdEditorRedraw(c *privilegedCommandImpl, e *editor, w *window, args ...string) { 404 | wicore.Go("viewReady", func() { 405 | e.viewReady <- true 406 | }) 407 | } 408 | 409 | // RegisterEditorDefaults registers the top-level native commands and key 410 | // bindings. 411 | func RegisterEditorDefaults(view wicore.ViewW) { 412 | cmds := []wicore.Command{ 413 | &wicore.CommandImpl{ 414 | "alert", 415 | 1, 416 | cmdAlert, 417 | wicore.WindowCategory, 418 | lang.Map{ 419 | lang.En: "Shows a modal message", 420 | }, 421 | lang.Map{ 422 | lang.En: "Prints a message in a modal dialog box.", 423 | }, 424 | }, 425 | &wicore.CommandImpl{ 426 | "editor_bootstrap_ui", 427 | 0, 428 | cmdEditorBootstrapUI, 429 | wicore.WindowCategory, 430 | lang.Map{ 431 | lang.En: "Bootstraps the editor's UI", 432 | }, 433 | lang.Map{ 434 | lang.En: "Bootstraps the editor's UI. This command is automatically run on startup and cannot be executed afterward. It adds the standard status bar. This command exists so it can be overriden by a plugin, so it can create its own status bar.", 435 | }, 436 | }, 437 | &wicore.CommandImpl{ 438 | "editor_command_window", 439 | 0, 440 | cmdEditorCommandWindow, 441 | wicore.CommandsCategory, 442 | lang.Map{ 443 | lang.En: "Shows the interactive command window", 444 | }, 445 | lang.Map{ 446 | lang.En: "This commands exists so it can be bound to a key to pop up the interactive command window.", 447 | }, 448 | }, 449 | &privilegedCommandImpl{ 450 | "editor_quit", 451 | -1, 452 | cmdEditorQuit, 453 | wicore.EditorCategory, 454 | lang.Map{ 455 | lang.En: "Quits", 456 | }, 457 | lang.Map{ 458 | lang.En: "Quits the editor. Use 'force' to bypasses writing the files to disk.", 459 | }, 460 | }, 461 | &privilegedCommandImpl{ 462 | "editor_redraw", 463 | 0, 464 | cmdEditorRedraw, 465 | wicore.EditorCategory, 466 | lang.Map{ 467 | lang.En: "Forcibly redraws the terminal", 468 | }, 469 | lang.Map{ 470 | lang.En: "Forcibly redraws the terminal.", 471 | }, 472 | }, 473 | &wicore.CommandAlias{"q", "editor_quit", nil}, 474 | &wicore.CommandAlias{"q!", "editor_quit", []string{"force"}}, 475 | &wicore.CommandAlias{"quit", "editor_quit", nil}, 476 | } 477 | commands := view.CommandsW() 478 | for _, cmd := range cmds { 479 | commands.Register(cmd) 480 | } 481 | 482 | bindings := view.KeyBindingsW() 483 | bindings.Set(wicore.AllMode, key.Press{Key: key.F1}, "help") 484 | bindings.Set(wicore.AllMode, key.Press{Ch: ':'}, "editor_command_window") 485 | bindings.Set(wicore.AllMode, key.Press{Ctrl: true, Ch: 'c'}, "quit") 486 | bindings.Set(wicore.Insert, key.Press{Key: key.Escape}, "key_set_normal") 487 | } 488 | -------------------------------------------------------------------------------- /editor/editor_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "io/ioutil" 9 | "log" 10 | "testing" 11 | 12 | "github.com/maruel/ut" 13 | "github.com/wi-ed/wi/wicore" 14 | "github.com/wi-ed/wi/wicore/colors" 15 | "github.com/wi-ed/wi/wicore/raster" 16 | ) 17 | 18 | func init() { 19 | // TODO(maruel): This has persistent side-effect. Figure out how to handle 20 | // "log" properly. Likely by using the same mechanism as used in package 21 | // "subcommands". 22 | log.SetOutput(ioutil.Discard) 23 | } 24 | 25 | // TODO(maruel): Add a test with small display (10x2) and ensure it's somewhat 26 | // usable. 27 | 28 | func keepLog(t *testing.T) func() { 29 | out := ut.NewWriter(t) 30 | log.SetOutput(out) 31 | return func() { 32 | log.SetOutput(ioutil.Discard) 33 | _ = out.Close() 34 | } 35 | } 36 | 37 | func compareBuffers(t *testing.T, expected *raster.Buffer, actual *raster.Buffer) { 38 | ut.AssertEqual(t, expected.Height, actual.Height) 39 | ut.AssertEqual(t, expected.Width, actual.Width) 40 | // First compares lines of text, then colors. 41 | for l := 0; l < expected.Height; l++ { 42 | e := expected.Line(l) 43 | a := actual.Line(l) 44 | ut.AssertEqualIndex(t, l, string(e.Runes()), string(a.Runes())) 45 | ut.AssertEqualIndex(t, l, e.Formats(), a.Formats()) 46 | } 47 | } 48 | 49 | func TestMainImmediateQuit(t *testing.T) { 50 | defer keepLog(t)() 51 | 52 | terminal := NewTerminalFake(80, 25, []TerminalEvent{}) 53 | editor, err := MakeEditor(terminal, true) 54 | ut.AssertEqual(t, nil, err) 55 | defer func() { 56 | _ = editor.Close() 57 | }() 58 | 59 | wicore.PostCommand(editor, nil, "editor_bootstrap_ui") 60 | wicore.PostCommand(editor, nil, "new") 61 | // Supporting this command requires using "go test -tags debug" 62 | // wicore.PostCommand(editor, nil, "log_all") 63 | wicore.PostCommand(editor, nil, "editor_quit") 64 | ut.AssertEqual(t, 0, editor.EventLoop()) 65 | 66 | expected := raster.NewBuffer(80, 25) 67 | expected.Fill(raster.MakeCell(' ', colors.BrightYellow, colors.Black)) 68 | expected.DrawString("Dummy content", 0, 0, raster.CellFormat{Fg: colors.BrightYellow, Bg: colors.Black}) 69 | expected.DrawString("Really", 0, 1, raster.CellFormat{Fg: colors.BrightYellow, Bg: colors.Black}) 70 | expected.DrawString("Status Name Normal 0,0 ", 0, 24, raster.CellFormat{Fg: colors.Red, Bg: colors.LightGray}) 71 | expected.Cell(0, 0).F.Bg = colors.White 72 | expected.Cell(0, 0).F.Fg = colors.Black 73 | compareBuffers(t, expected, terminal.Buffer) 74 | } 75 | 76 | func TestMainInvalidThenQuit(t *testing.T) { 77 | defer keepLog(t)() 78 | 79 | terminal := NewTerminalFake(80, 25, []TerminalEvent{}) 80 | editor, err := MakeEditor(terminal, true) 81 | ut.AssertEqual(t, nil, err) 82 | defer func() { 83 | _ = editor.Close() 84 | }() 85 | 86 | wicore.PostCommand(editor, nil, "editor_bootstrap_ui") 87 | wicore.PostCommand(editor, nil, "invalid") 88 | wicore.PostCommand(editor, nil, "editor_quit") 89 | ut.AssertEqual(t, 0, editor.EventLoop()) 90 | 91 | expected := raster.NewBuffer(80, 25) 92 | expected.Fill(raster.MakeCell(' ', colors.Red, colors.Black)) 93 | expected.DrawString("Root", 0, 0, raster.CellFormat{Fg: colors.Red, Bg: colors.Black}) 94 | expected.DrawString("Status Name Normal Status Position", 0, 24, raster.CellFormat{Fg: colors.Red, Bg: colors.LightGray}) 95 | compareBuffers(t, expected, terminal.Buffer) 96 | } 97 | -------------------------------------------------------------------------------- /editor/keybindings.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "github.com/wi-ed/wi/wicore" 9 | "github.com/wi-ed/wi/wicore/key" 10 | "github.com/wi-ed/wi/wicore/lang" 11 | ) 12 | 13 | type keyBindings struct { 14 | normalMappings map[key.Press]string 15 | insertMappings map[key.Press]string 16 | } 17 | 18 | func (k *keyBindings) Set(mode wicore.KeyboardMode, key key.Press, cmdName string) bool { 19 | if !key.IsValid() { 20 | return false 21 | } 22 | var ok bool 23 | if mode == wicore.AllMode || mode == wicore.Normal { 24 | _, ok = k.normalMappings[key] 25 | k.normalMappings[key] = cmdName 26 | } 27 | if mode == wicore.AllMode || mode == wicore.Insert { 28 | _, ok = k.insertMappings[key] 29 | k.insertMappings[key] = cmdName 30 | } 31 | return !ok 32 | } 33 | 34 | func (k *keyBindings) Get(mode wicore.KeyboardMode, key key.Press) string { 35 | if !key.IsValid() { 36 | return "" 37 | } 38 | if mode == wicore.Normal || mode == wicore.AllMode { 39 | if v, ok := k.normalMappings[key]; ok { 40 | return v 41 | } 42 | } 43 | if mode == wicore.Insert || mode == wicore.AllMode { 44 | if v, ok := k.insertMappings[key]; ok { 45 | return v 46 | } 47 | } 48 | return "" 49 | } 50 | 51 | func (k *keyBindings) GetAssigned(mode wicore.KeyboardMode) []key.Press { 52 | out := []key.Press{} 53 | if mode == wicore.Normal || mode == wicore.AllMode { 54 | for k := range k.normalMappings { 55 | out = append(out, k) 56 | } 57 | } 58 | if mode == wicore.Insert || mode == wicore.AllMode { 59 | for k := range k.insertMappings { 60 | out = append(out, k) 61 | } 62 | } 63 | return out 64 | } 65 | 66 | func makeKeyBindings() wicore.KeyBindingsW { 67 | return &keyBindings{make(map[key.Press]string), make(map[key.Press]string)} 68 | } 69 | 70 | // Commands. 71 | 72 | func cmdKeyBind(c *wicore.CommandImpl, e wicore.EditorW, w wicore.Window, args ...string) { 73 | location := args[0] 74 | modeName := args[1] 75 | keyName := args[2] 76 | cmdName := args[3] 77 | 78 | if location == "global" { 79 | w = wicore.RootWindow(w) 80 | } else if location != "window" { 81 | cmd := wicore.GetCommand(e, w, "key_bind") 82 | e.ExecuteCommand(w, "alert", cmd.LongDesc()) 83 | return 84 | } 85 | 86 | var mode wicore.KeyboardMode 87 | if modeName == "command" { 88 | mode = wicore.Normal 89 | } else if modeName == "edit" { 90 | mode = wicore.Normal 91 | } else if modeName == "all" { 92 | mode = wicore.AllMode 93 | } else { 94 | cmd := wicore.GetCommand(e, w, "key_bind") 95 | e.ExecuteCommand(w, "alert", cmd.LongDesc()) 96 | return 97 | } 98 | // TODO(maruel): Refuse invalid keyName. 99 | k := key.StringToPress(keyName) 100 | // TODO(maruel): Handle views in different process? 101 | viewW, ok := w.View().(wicore.ViewW) 102 | if !ok { 103 | e.ExecuteCommand(w, "alert", "internal failure") 104 | return 105 | } 106 | viewW.KeyBindingsW().Set(mode, k, cmdName) 107 | } 108 | 109 | // RegisterKeyBindingCommands registers the keyboard mapping related commands. 110 | func RegisterKeyBindingCommands(dispatcher wicore.CommandsW) { 111 | cmds := []wicore.Command{ 112 | &wicore.CommandImpl{ 113 | "key_bind", 114 | 4, 115 | cmdKeyBind, 116 | wicore.CommandsCategory, 117 | lang.Map{ 118 | lang.En: "Binds a keyboard mapping to a command", 119 | }, 120 | lang.Map{ 121 | lang.En: "Usage: key_bind [window|global] [command|edit|all] \nBinds a keyboard mapping to a command. The binding can be to the active view for view-specific key binding or to the root view for global key bindings.", 122 | }, 123 | }, 124 | } 125 | for _, cmd := range cmds { 126 | dispatcher.Register(cmd) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /editor/plugins.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net/rpc" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "runtime" 17 | "strings" 18 | "sync" 19 | "time" 20 | 21 | "github.com/wi-ed/wi/wicore" 22 | "github.com/wi-ed/wi/wicore/lang" 23 | ) 24 | 25 | // pluginProcess represents an out-of-process plugin. 26 | type pluginProcess struct { 27 | lock sync.Mutex 28 | proc *os.Process 29 | client *rpc.Client // All communication goes through this object. 30 | pid int // Also stored here in case proc is nil. It is not reset even when the process is closed. 31 | details wicore.PluginDetails // Initialized early by sync call GetInfo(). 32 | initialized bool // Initialized late after async call Init() completed. 33 | err error // If set, the plugin had an error and is quarantined. 34 | listener wicore.EventListener 35 | } 36 | 37 | func (p *pluginProcess) Close() error { 38 | p.lock.Lock() 39 | defer p.lock.Unlock() 40 | var err error 41 | if p.listener != nil { 42 | err = p.listener.Close() 43 | p.listener = nil 44 | } 45 | if p.client != nil { 46 | tmp := 0 47 | call := p.client.Go("PluginRPC.Quit", 0, &tmp, nil) 48 | select { 49 | case <-call.Done: 50 | if call.Error != nil { 51 | err = call.Error 52 | } 53 | case <-time.After(time.Second): 54 | err = fmt.Errorf("%s timed out", p) 55 | } 56 | if err2 := p.client.Close(); err2 != nil { 57 | err = err2 58 | } 59 | p.client = nil 60 | } 61 | if p.proc != nil { 62 | if err1 := p.proc.Kill(); err1 != nil { 63 | err = err1 64 | } 65 | p.proc = nil 66 | } 67 | log.Printf("%s.Close()", p) 68 | if err != nil && p.err == nil { 69 | p.err = err 70 | } 71 | return err 72 | } 73 | 74 | func (p *pluginProcess) String() string { 75 | return fmt.Sprintf("Plugin(%s, %d)", p.details.Name, p.pid) 76 | } 77 | 78 | func (p *pluginProcess) Details() wicore.PluginDetails { 79 | return p.details 80 | } 81 | 82 | // Init asynchronously initializes the plugin. 83 | func (p *pluginProcess) Init(e wicore.Editor) { 84 | log.Printf("%s.Init()", p) 85 | p.lock.Lock() 86 | defer p.lock.Unlock() 87 | 88 | // Make sure all plugins have their event registry properly registered 89 | // before doing anything silly. This is purely a process-local setup. 90 | p.listener = registerPluginEvents(p.client, e) 91 | 92 | out := 0 93 | details := wicore.EditorDetails{ 94 | e.ID(), 95 | e.Version(), 96 | } 97 | call := p.client.Go("PluginRPC.Init", details, &out, nil) 98 | wicore.Go("PluginRPC.Init", func() { 99 | _ = <-call.Done 100 | p.lock.Lock() 101 | defer p.lock.Unlock() 102 | p.initialized = true 103 | if call.Error != nil && p.err == nil { 104 | p.err = call.Error 105 | log.Printf("%s.Init() failed: %s", p, p.err) 106 | _ = p.listener.Close() 107 | p.listener = nil 108 | } else { 109 | log.Printf("%s.Init() done", p) 110 | } 111 | }) 112 | } 113 | 114 | // Plugins is the collection of Plugin instances, it represents all the live 115 | // plugin processes. 116 | type Plugins []wicore.Plugin 117 | 118 | // Close implements io.Closer. 119 | func (p Plugins) Close() error { 120 | var out error 121 | for _, instance := range p { 122 | if err := instance.Close(); err != nil { 123 | out = err 124 | } 125 | } 126 | return out 127 | } 128 | 129 | // loadPlugin starts a plugin and returns the process. 130 | func loadPlugin(cmdLine []string) (wicore.Plugin, error) { 131 | log.Printf("loadPlugin(%v)", cmdLine) 132 | cmd := exec.Command(cmdLine[0], cmdLine[1:]...) 133 | cmd.Env = append(os.Environ(), "WI=plugin") 134 | 135 | stdin, err := cmd.StdinPipe() 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | stdout, err := cmd.StdoutPipe() 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | stderr, err := cmd.StderrPipe() 146 | if err != nil { 147 | return nil, err 148 | } 149 | if err := cmd.Start(); err != nil { 150 | return nil, err 151 | } 152 | 153 | first := make(chan error) 154 | 155 | // Fail on any write to Stderr. 156 | wicore.Go("stderrReader", func() { 157 | buf := make([]byte, 2048) 158 | n, _ := stderr.Read(buf) 159 | if n != 0 { 160 | first <- fmt.Errorf("plugin %v failed: %s", cmdLine, buf[:n]) 161 | } 162 | }) 163 | 164 | wicore.Go("stdoutReader", func() { 165 | // Before starting the RPC, ensures the version matches. 166 | expectedVersion := wicore.CalculateVersion() 167 | b := make([]byte, len(expectedVersion)) 168 | if _, err := stdout.Read(b); err != nil { 169 | first <- err 170 | } 171 | actualVersion := string(b) 172 | if expectedVersion != actualVersion { 173 | first <- fmt.Errorf("unexpected wicore version; expected %s, got %s", expectedVersion, actualVersion) 174 | } 175 | first <- nil 176 | }) 177 | 178 | err = <-first 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | conn := wicore.MakeReadWriteCloser(stdout, stdin) 184 | client := rpc.NewClient(conn) 185 | p := &pluginProcess{ 186 | sync.Mutex{}, 187 | cmd.Process, 188 | client, 189 | cmd.Process.Pid, 190 | wicore.PluginDetails{"", ""}, 191 | false, 192 | nil, 193 | nil, 194 | } 195 | if err = p.client.Call("PluginRPC.GetInfo", lang.Active(), &p.details); err != nil { 196 | return nil, err 197 | } 198 | log.Printf("%s is now functional", p) 199 | return p, nil 200 | } 201 | 202 | func parseDir(i string) (string, error) { 203 | abs, err := filepath.Abs(i) 204 | if err != nil { 205 | return "", fmt.Errorf("invalid path %s: %s", i, err) 206 | } 207 | f, err := os.Stat(abs) 208 | if err != nil { 209 | return "", fmt.Errorf("could not stat %s: %s", i, err) 210 | } 211 | if !f.IsDir() { 212 | return "", fmt.Errorf("%s is not a directory", i) 213 | } 214 | return abs, nil 215 | } 216 | 217 | // getPluginsPaths returns the search paths for plugins. 218 | // 219 | // Currently look at ".", each element of $GOPATH/bin and in $WIPLUGINSPATH. 220 | func getPluginsPaths() []string { 221 | out := []string{} 222 | for _, i := range filepath.SplitList(os.Getenv("GOPATH")) { 223 | abs, err := parseDir(filepath.Join(i, "bin")) 224 | if err != nil { 225 | log.Printf("GOPATH contains invalid %s: %s", i, err) 226 | continue 227 | } 228 | out = append(out, abs) 229 | } 230 | for _, i := range filepath.SplitList(os.Getenv("WIPLUGINSPATH")) { 231 | abs, err := parseDir(i) 232 | if err != nil { 233 | log.Printf("WIPLUGINSPATH contains invalid %s: %s", i, err) 234 | continue 235 | } 236 | out = append(out, abs) 237 | } 238 | log.Printf("getPluginsPaths() = %v", out) 239 | return out 240 | } 241 | 242 | // enumPlugins enumerate the plugins that should be loaded. 243 | // 244 | // It returns the command lines to use to start the processes. It support 245 | // normal executable, standalone source file and directory containing multiple 246 | // source files. 247 | // 248 | // Source files will incur a ~500ms to ~1s compilation overhead, so they should 249 | // eventually be compiled. Still, it's very useful for quick prototyping. 250 | func enumPlugins(searchDirs []string) ([][]string, error) { 251 | out := [][]string{} 252 | var err error 253 | for _, searchDir := range searchDirs { 254 | files, err2 := ioutil.ReadDir(searchDir) 255 | if err2 != nil { 256 | err = err2 257 | } 258 | if len(files) == 0 { 259 | continue 260 | } 261 | 262 | for _, f := range files { 263 | name := f.Name() 264 | if !strings.HasPrefix(name, "wi-plugin-") { 265 | continue 266 | } 267 | filePath := filepath.Join(searchDir, name) 268 | 269 | if f.IsDir() { 270 | // Compile on-the-fly a directory of source files. 271 | // TODO(maruel): When built with -tags debug, pass it along. 272 | files, err2 := filepath.Glob(filepath.Join(filePath, "*.go")) 273 | if len(files) == 0 || err2 != nil { 274 | continue 275 | } 276 | i := []string{"go", "run"} 277 | for _, t := range files { 278 | i = append(i, t) 279 | } 280 | out = append(out, i) 281 | continue 282 | } 283 | 284 | if strings.HasSuffix(name, ".go") { 285 | // Compile on-the-fly a source file. 286 | // TODO(maruel): When built with -tags debug, pass it along. 287 | out = append(out, []string{"go", "run", filePath}) 288 | continue 289 | } 290 | 291 | // Crude check for executable test. 292 | if runtime.GOOS == "windows" { 293 | if !strings.HasSuffix(name, ".exe") { 294 | continue 295 | } 296 | } else { 297 | if f.Mode()&0111 == 0 { 298 | continue 299 | } 300 | } 301 | out = append(out, []string{filePath}) 302 | } 303 | } 304 | return out, err 305 | } 306 | 307 | // loadPlugins loads all plugins simultaneously and only returns once they are 308 | // all loaded. At that point a single RPC call (GetInfo()) was done so they are 309 | // not yet fully initialized. It's up to the caller to call Init() on each 310 | // plugin. 311 | func loadPlugins(pluginExecutables [][]string) (Plugins, error) { 312 | type x struct { 313 | wicore.Plugin 314 | error 315 | } 316 | c := make(chan x) 317 | wicore.Go("loadPlugins", func() { 318 | var wg sync.WaitGroup 319 | for _, cmd := range pluginExecutables { 320 | wg.Add(1) 321 | wicore.Go("loadPlugin", func() { 322 | func(n []string) { 323 | defer wg.Done() 324 | if p, err := loadPlugin(n); err != nil { 325 | c <- x{error: fmt.Errorf("failed to load %v: %s", n, err)} 326 | } else { 327 | c <- x{Plugin: p} 328 | } 329 | }(cmd) 330 | }) 331 | } 332 | // Wait for all the plugins to be loaded. 333 | wg.Wait() 334 | close(c) 335 | }) 336 | 337 | // Convert to a slice. 338 | var wg sync.WaitGroup 339 | out := make(Plugins, 0, len(pluginExecutables)) 340 | var errs []error 341 | wg.Add(1) 342 | wicore.Go("pluginReaper", func() { 343 | defer wg.Done() 344 | for i := range c { 345 | if i.error != nil { 346 | errs = append(errs, i.error) 347 | } else { 348 | out = append(out, i.Plugin) 349 | } 350 | } 351 | }) 352 | wg.Wait() 353 | 354 | var err error 355 | if len(errs) != 0 { 356 | tmp := "" 357 | for _, e := range errs { 358 | tmp += e.Error() + "\n" 359 | } 360 | err = errors.New(tmp[:len(tmp)-1]) 361 | } 362 | return out, err 363 | } 364 | -------------------------------------------------------------------------------- /editor/strings.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "github.com/wi-ed/wi/wicore/lang" 9 | ) 10 | 11 | var activateDisabled = lang.Map{ 12 | lang.En: "Can't activate a disabled view.", 13 | } 14 | 15 | var cantAddTwoWindowWithSameDocking = lang.Map{ 16 | lang.En: "Can't create two windows with the same docking \"%s\".", 17 | } 18 | 19 | var invalidDocking = lang.Map{ 20 | lang.En: "String \"%s\" does not refer to a valid Docking type.", 21 | } 22 | 23 | var invalidRect = lang.Map{ 24 | lang.En: "\"%s, %s, %s, %s\" does not refer to a valid Rect.", 25 | } 26 | 27 | var invalidViewFactory = lang.Map{ 28 | lang.En: "\"%s\" does not refer to a valid ViewFactory. Make sure the view factory was properly registered.", 29 | } 30 | 31 | var isNotValidWindow = lang.Map{ 32 | lang.En: "ID \"%s\" does not refer to a valid window ID.", 33 | } 34 | 35 | var notFound = lang.Map{ 36 | lang.En: "Command \"%s\" is not registered.", 37 | } 38 | 39 | // notMapped describes that a key is not mapped to any command. 40 | var notMapped = lang.Map{ 41 | lang.En: "\"%s\" is not mapped to any command.", 42 | } 43 | 44 | var viewDirty = lang.Map{ 45 | lang.En: "View \"%s\" is not saved, aborting quit.", 46 | } 47 | -------------------------------------------------------------------------------- /editor/terminal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "github.com/wi-ed/wi/wicore" 9 | "github.com/wi-ed/wi/wicore/key" 10 | "github.com/wi-ed/wi/wicore/raster" 11 | ) 12 | 13 | // Terminal is the interface to the actual terminal termbox so it can be mocked 14 | // in unit test or a different implementation than termbox can be used. 15 | type Terminal interface { 16 | // Size returns the current size of the terminal window. 17 | Size() (int, int) 18 | 19 | // SeedEvents() returns a channel where events will be sent to. 20 | // 21 | // The channel will be closed when the terminal is closed. 22 | SeedEvents() <-chan TerminalEvent 23 | 24 | // Blit updates the terminal output with the buffer specified. 25 | // 26 | // It is important for the buffer to be the right size, otherwise the display 27 | // will be partially updated. 28 | Blit(b *raster.Buffer) 29 | 30 | // SetCursor moves the cursor to a position. 31 | SetCursor(col, row int) 32 | } 33 | 34 | // EventType is the type of supported terminal event. 35 | type EventType int 36 | 37 | // Supported event types. 38 | const ( 39 | EventKey = iota 40 | EventResize 41 | ) 42 | 43 | // TerminalEvent represents an event that occured on the terminal. 44 | type TerminalEvent struct { 45 | Type EventType // Type determines which other member will be valid for this event. 46 | Key key.Press 47 | Size Size 48 | } 49 | 50 | // Size represents the size of an UI element. 51 | type Size struct { 52 | Width int 53 | Height int 54 | } 55 | 56 | // Logger is the interface to log to. It must be used instead of 57 | // log.Logger.Printf() or testing.T.Log(). This permits to collect logs for a 58 | // complete test case. 59 | // 60 | // TODO(maruel): Move elsewhere. 61 | type Logger interface { 62 | Logf(format string, v ...interface{}) 63 | } 64 | 65 | // TerminalFake implements the Terminal and buffers the output. 66 | // 67 | // It is mostly useful in unit tests. 68 | type TerminalFake struct { 69 | Width int 70 | Height int 71 | Events []TerminalEvent 72 | Buffer *raster.Buffer 73 | } 74 | 75 | // Size implements Terminal. 76 | func (t *TerminalFake) Size() (int, int) { 77 | return t.Width, t.Height 78 | } 79 | 80 | // SeedEvents implements Terminal. 81 | func (t *TerminalFake) SeedEvents() <-chan TerminalEvent { 82 | out := make(chan TerminalEvent) 83 | wicore.Go("SeedEvents", func() { 84 | for _, i := range t.Events { 85 | out <- i 86 | } 87 | }) 88 | return out 89 | } 90 | 91 | // Blit implements Terminal. 92 | func (t *TerminalFake) Blit(b *raster.Buffer) { 93 | t.Buffer.Blit(b) 94 | } 95 | 96 | // SetCursor implements Terminal. 97 | func (t *TerminalFake) SetCursor(col, line int) { 98 | // TODO(maruel): Implement somehow. 99 | } 100 | 101 | // NewTerminalFake returns an initialized TerminalFake which implements the 102 | // interface Terminal. 103 | // 104 | // The terminal can be preloaded with fake events. 105 | func NewTerminalFake(width, height int, events []TerminalEvent) *TerminalFake { 106 | return &TerminalFake{ 107 | width, 108 | height, 109 | events, 110 | raster.NewBuffer(width, height), 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /editor/view.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "time" 11 | "unicode/utf8" 12 | 13 | "github.com/wi-ed/wi/wicore" 14 | "github.com/wi-ed/wi/wicore/colors" 15 | "github.com/wi-ed/wi/wicore/raster" 16 | ) 17 | 18 | // TODO(maruel): Likely move into wicore for reuse. 19 | type view struct { 20 | commands wicore.CommandsW 21 | keyBindings wicore.KeyBindingsW 22 | eventRegistry wicore.EventRegistry 23 | id int 24 | title string 25 | isDisabled bool 26 | naturalX int // Desired size. 27 | naturalY int 28 | actualX int // Actual size in UI. 29 | actualY int 30 | window wicore.Window 31 | onAttach func(v *view, w wicore.Window) 32 | defaultFormat raster.CellFormat 33 | buffer *raster.Buffer 34 | events []wicore.EventListener 35 | } 36 | 37 | // wicore.View interface. 38 | 39 | func (v *view) ID() string { 40 | return fmt.Sprintf("view:%d", v.id) 41 | } 42 | 43 | func (v *view) String() string { 44 | return fmt.Sprintf("View(%s)", v.title) 45 | } 46 | 47 | func (v *view) Close() error { 48 | var err error 49 | for _, event := range v.events { 50 | err2 := event.Close() 51 | if err2 != nil { 52 | err = err2 53 | } 54 | } 55 | return err 56 | } 57 | 58 | func (v *view) Commands() wicore.Commands { 59 | return v.commands 60 | } 61 | 62 | func (v *view) CommandsW() wicore.CommandsW { 63 | return v.commands 64 | } 65 | 66 | func (v *view) KeyBindings() wicore.KeyBindings { 67 | return v.keyBindings 68 | } 69 | 70 | func (v *view) KeyBindingsW() wicore.KeyBindingsW { 71 | return v.keyBindings 72 | } 73 | 74 | func (v *view) Title() string { 75 | return v.title 76 | } 77 | 78 | func (v *view) IsDisabled() bool { 79 | return v.isDisabled 80 | } 81 | 82 | func (v *view) NaturalSize() (x, y int) { 83 | return v.naturalX, v.naturalY 84 | } 85 | 86 | func (v *view) SetSize(x, y int) { 87 | log.Printf("View(%s).SetSize(%d, %d)", v.Title(), x, y) 88 | v.actualX = x 89 | v.actualY = y 90 | v.buffer = raster.NewBuffer(x, y) 91 | } 92 | 93 | func (v *view) OnAttach(w wicore.Window) { 94 | if v.onAttach != nil { 95 | v.onAttach(v, w) 96 | } 97 | v.window = w 98 | } 99 | 100 | // DefaultFormat returns the View's format or the parent Window's View's format. 101 | func (v *view) DefaultFormat() raster.CellFormat { 102 | if v.defaultFormat.Empty() && v.window != nil { 103 | w := v.window.Parent() 104 | if w != nil { 105 | return w.View().DefaultFormat() 106 | } 107 | } 108 | return v.defaultFormat 109 | } 110 | 111 | // A disabled static view. 112 | type staticDisabledView struct { 113 | view 114 | } 115 | 116 | func (v *staticDisabledView) Buffer() *raster.Buffer { 117 | // TODO(maruel): Use the parent view format by default. No idea how to 118 | // surface this information here. Cost is at least a RPC, potentially 119 | // multiple when multiple plugins are involved in the tree. 120 | v.buffer.Fill(raster.Cell{' ', v.DefaultFormat()}) 121 | v.buffer.DrawString(v.Title(), 0, 0, v.DefaultFormat()) 122 | return v.buffer 123 | } 124 | 125 | // Empty non-editable window. 126 | func makeStaticDisabledView(e wicore.Editor, id int, title string, naturalX, naturalY int) *staticDisabledView { 127 | return &staticDisabledView{ 128 | view{ 129 | commands: makeCommands(), 130 | keyBindings: makeKeyBindings(), 131 | eventRegistry: e, 132 | id: id, 133 | title: title, 134 | isDisabled: true, 135 | naturalX: naturalX, 136 | naturalY: naturalY, 137 | defaultFormat: raster.CellFormat{Fg: colors.Red, Bg: colors.Black}, 138 | events: []wicore.EventListener{}, 139 | }, 140 | } 141 | } 142 | 143 | // The status line is a hierarchy of Window, one for each element, each showing 144 | // a single item. 145 | func statusRootViewFactory(e wicore.Editor, id int, args ...string) wicore.ViewW { 146 | // TODO(maruel): OnResize(), query the root Window size, if y<=5 or x<=15, 147 | // set the root status Window to y=0, so that it becomes effectively 148 | // invisible when the editor window is too small. 149 | v := makeStaticDisabledView(e, id, "Status Root", 1, 1) 150 | v.defaultFormat.Bg = colors.LightGray 151 | v.onAttach = func(v *view, w wicore.Window) { 152 | id := w.ID() 153 | e.TriggerCommands( 154 | wicore.EnqueuedCommands{ 155 | [][]string{ 156 | {"window_new", id, "left", "status_active_window_name"}, 157 | {"window_new", id, "right", "status_position"}, 158 | {"window_new", id, "fill", "status_mode"}, 159 | }, 160 | nil, 161 | }) 162 | } 163 | return v 164 | } 165 | 166 | func statusActiveWindowNameViewFactory(e wicore.Editor, id int, args ...string) wicore.ViewW { 167 | // Active Window View name. 168 | // TODO(maruel): Register events of Window activation, make itself Invalidate(). 169 | v := makeStaticDisabledView(e, id, "Status Name", 15, 1) 170 | v.defaultFormat = raster.CellFormat{} 171 | return v 172 | } 173 | 174 | func statusModeViewFactory(e wicore.Editor, id int, args ...string) wicore.ViewW { 175 | // Mostly for testing purpose, will contain the current mode "Insert" or "Command". 176 | v := makeStaticDisabledView(e, id, e.KeyboardMode().String(), 10, 1) 177 | v.defaultFormat = raster.CellFormat{} 178 | event := e.RegisterEditorKeyboardModeChanged(func(mode wicore.KeyboardMode) { 179 | v.title = mode.String() 180 | }) 181 | v.events = append(v.events, event) 182 | return v 183 | } 184 | 185 | func statusPositionViewFactory(e wicore.Editor, id int, args ...string) wicore.ViewW { 186 | // Position, % of file. 187 | // TODO(maruel): Register events of movement, make itself Invalidate(). 188 | v := makeStaticDisabledView(e, id, "Status Position", 15, 1) 189 | v.defaultFormat = raster.CellFormat{} 190 | event := e.RegisterDocumentCursorMoved(func(doc wicore.Document, col, row int) { 191 | v.title = fmt.Sprintf("%d,%d", col, row) 192 | }) 193 | v.events = append(v.events, event) 194 | return v 195 | } 196 | 197 | func infobarAlertViewFactory(e wicore.Editor, id int, args ...string) wicore.ViewW { 198 | out := "Alert: " + args[0] 199 | l := utf8.RuneCountInString(out) 200 | v := makeStaticDisabledView(e, id, out, l, 1) 201 | v.onAttach = func(v *view, w wicore.Window) { 202 | wicore.Go("infobarAlert", func() { 203 | // Dismiss after 5 seconds. 204 | <-time.After(5 * time.Second) 205 | wicore.PostCommand(e, nil, "window_close", w.ID()) 206 | }) 207 | } 208 | return v 209 | } 210 | 211 | // RegisterDefaultViewFactories registers the builtins views factories. 212 | func RegisterDefaultViewFactories(e Editor) { 213 | e.RegisterViewFactory("command", commandViewFactory) 214 | e.RegisterViewFactory("infobar_alert", infobarAlertViewFactory) 215 | e.RegisterViewFactory("new_document", documentViewFactory) 216 | e.RegisterViewFactory("status_active_window_name", statusActiveWindowNameViewFactory) 217 | e.RegisterViewFactory("status_mode", statusModeViewFactory) 218 | e.RegisterViewFactory("status_position", statusPositionViewFactory) 219 | e.RegisterViewFactory("status_root", statusRootViewFactory) 220 | } 221 | 222 | // Commands 223 | 224 | // RegisterViewCommands registers view-related commands 225 | func RegisterViewCommands(dispatcher wicore.CommandsW) { 226 | cmds := []wicore.Command{} 227 | for _, cmd := range cmds { 228 | dispatcher.Register(cmd) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /editor/window.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package editor 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/wi-ed/wi/wicore" 14 | "github.com/wi-ed/wi/wicore/colors" 15 | "github.com/wi-ed/wi/wicore/lang" 16 | "github.com/wi-ed/wi/wicore/raster" 17 | ) 18 | 19 | var singleBorder = []rune{'\u2500', '\u2502', '\u250D', '\u2510', '\u2514', '\u2518'} 20 | var doubleBorder = []rune{'\u2550', '\u2551', '\u2554', '\u2557', '\u255a', '\u255d'} 21 | 22 | type drawnBorder int 23 | 24 | const ( 25 | drawnBorderNone drawnBorder = 0 26 | drawnBorderLeft = 1 << iota 27 | drawnBorderRight 28 | drawnBorderTop 29 | drawnBorderBottom 30 | drawnBorderAll = drawnBorderLeft | drawnBorderRight | drawnBorderTop | drawnBorderBottom 31 | ) 32 | 33 | // window implements wicore.Window. It keeps its own buffer of its display. 34 | // 35 | // window is the only structure guaranteed to be in the wi main process. View 36 | // and Documents can be served via plugins. 37 | type window struct { 38 | id int // window ID relative to the parent. 39 | lastChildID int // last ID used for a children window. 40 | parent *window 41 | e wicore.Editor 42 | childrenWindows []*window 43 | windowBuffer *raster.Buffer // includes the border 44 | rect raster.Rect // Window Rect as described in wicore.Window.Rect(). 45 | clientAreaRect raster.Rect // Usable area within the Window, the part not obscured by borders. 46 | viewRect raster.Rect // Window View Rect, which is the client area not used by childrenWindows. 47 | view wicore.ViewW // View that renders the content. It may be nil if this Window has no content. 48 | docking wicore.DockingType 49 | border wicore.BorderType 50 | effectiveBorder drawnBorder // effectiveBorder automatically collapses borders when the Window Rect is too small and is based on docking. 51 | borderFormat raster.CellFormat // Format to be used in borders. It can be different from .View().DefaultFormat(). 52 | } 53 | 54 | // wicore.Window interface. 55 | 56 | func (w *window) String() string { 57 | return fmt.Sprintf("Window(%s, %s, %v)", w.ID(), w.View().Title(), w.Rect()) 58 | } 59 | 60 | func (w *window) ID() string { 61 | if w.parent == nil { 62 | // editor.rootWindow.id is always 0. 63 | return fmt.Sprintf("%d", w.id) 64 | } 65 | return fmt.Sprintf("%s:%d", w.parent.ID(), w.id) 66 | } 67 | 68 | func (w *window) Parent() wicore.Window { 69 | // TODO(maruel): Understand why this is necessary at all. 70 | if w.parent != nil { 71 | return w.parent 72 | } 73 | return nil 74 | } 75 | 76 | func (w *window) ChildrenWindows() []wicore.Window { 77 | out := make([]wicore.Window, len(w.childrenWindows)) 78 | for i, v := range w.childrenWindows { 79 | out[i] = v 80 | } 81 | return out 82 | } 83 | 84 | func (w *window) Rect() raster.Rect { 85 | return w.rect 86 | } 87 | 88 | func (w *window) Docking() wicore.DockingType { 89 | return w.docking 90 | } 91 | 92 | func (w *window) View() wicore.View { 93 | return w.view 94 | } 95 | 96 | // Private methods. 97 | 98 | // Recursively detach a window tree. 99 | func detachRecursively(w *window) { 100 | for _, c := range w.childrenWindows { 101 | detachRecursively(c) 102 | } 103 | w.parent = nil 104 | w.childrenWindows = nil 105 | } 106 | 107 | func recurseIDToWindow(w *window, fullID string) *window { 108 | parts := strings.SplitN(fullID, ":", 2) 109 | intID, err := strconv.Atoi(parts[0]) 110 | if err != nil { 111 | // Element is not a valid number, it's an invalid reference. 112 | return nil 113 | } 114 | for _, child := range w.childrenWindows { 115 | if child.id == intID { 116 | if len(parts) == 2 { 117 | return recurseIDToWindow(child, parts[1]) 118 | } 119 | return child 120 | } 121 | } 122 | return nil 123 | } 124 | 125 | // Converts a wicore.Window.ID() to a window pointer. Returns nil if invalid. 126 | // 127 | // "0" is the special reference to the root window. 128 | func (e *editor) idToWindow(id string) *window { 129 | cur := e.rootWindow 130 | if id != "0" { 131 | if !strings.HasPrefix(id, "0:") { 132 | log.Printf("Invalid id: %s", id) 133 | return nil 134 | } 135 | cur = recurseIDToWindow(cur, id[2:]) 136 | } 137 | return cur 138 | } 139 | 140 | // setRect sets the rect of this Window, based on the parent's Window own 141 | // Rect(). It updates Rect() and synchronously updates the child Window that 142 | // are not DockingFloating. 143 | func (w *window) setRect(rect raster.Rect) { 144 | // setRect() recreates the buffer and immediately draws the borders. 145 | if w.rect != rect { 146 | w.rect = rect 147 | // Internal consistency check. 148 | if w.parent != nil { 149 | if !w.rect.In(w.parent.clientAreaRect) { 150 | panic(fmt.Sprintf("Child %v doesn't fit parent's client area %v: %v", w, w.parent, w.parent.clientAreaRect)) 151 | } 152 | } 153 | 154 | w.windowBuffer = raster.NewBuffer(w.rect.Width, w.rect.Height) 155 | w.updateBorder() 156 | } 157 | // Still flow the call through children Window, so DockingFloating are 158 | // properly updated. 159 | w.resizeChildren() 160 | } 161 | 162 | // calculateEffectiveBorder calculates window.effectiveBorder. 163 | func calculateEffectiveBorder(r raster.Rect, d wicore.DockingType) drawnBorder { 164 | switch d { 165 | case wicore.DockingFill: 166 | return drawnBorderNone 167 | 168 | case wicore.DockingFloating: 169 | if r.Width >= 5 && r.Height >= 3 { 170 | return drawnBorderAll 171 | } 172 | return drawnBorderNone 173 | 174 | case wicore.DockingLeft: 175 | if r.Width > 1 && r.Height > 0 { 176 | return drawnBorderRight 177 | } 178 | return drawnBorderNone 179 | 180 | case wicore.DockingRight: 181 | if r.Width > 1 && r.Height > 0 { 182 | return drawnBorderLeft 183 | } 184 | return drawnBorderNone 185 | 186 | case wicore.DockingTop: 187 | if r.Height > 1 && r.Width > 0 { 188 | return drawnBorderBottom 189 | } 190 | return drawnBorderNone 191 | 192 | case wicore.DockingBottom: 193 | if r.Height > 1 && r.Width > 0 { 194 | return drawnBorderTop 195 | } 196 | return drawnBorderNone 197 | 198 | default: 199 | panic("Unknown DockingType") 200 | } 201 | } 202 | 203 | // resizeChildren() resizes all the children Window. 204 | func (w *window) resizeChildren() { 205 | log.Printf("%s.resizeChildren()", w) 206 | // When borders are used, w.clientAreaRect.X and .Y are likely 1. 207 | remaining := w.clientAreaRect 208 | var fill *window 209 | for _, child := range w.childrenWindows { 210 | switch child.Docking() { 211 | case wicore.DockingFill: 212 | fill = child 213 | 214 | case wicore.DockingFloating: 215 | // Floating uses its own thing. 216 | // TODO(maruel): Not clean. Doesn't handle root Window resize properly. 217 | child.setRect(child.Rect()) 218 | 219 | case wicore.DockingLeft: 220 | width, _ := child.View().NaturalSize() 221 | if width >= remaining.Width { 222 | width = remaining.Width 223 | } else if child.border != wicore.BorderNone { 224 | width++ 225 | } 226 | tmp := remaining 227 | tmp.Width = width 228 | remaining.X += width 229 | remaining.Width -= width 230 | child.setRect(tmp) 231 | 232 | case wicore.DockingRight: 233 | width, _ := child.View().NaturalSize() 234 | if width >= remaining.Width { 235 | width = remaining.Width 236 | } else if child.border != wicore.BorderNone { 237 | width++ 238 | } 239 | tmp := remaining 240 | tmp.X += (remaining.Width - width) 241 | tmp.Width = width 242 | remaining.Width -= width 243 | child.setRect(tmp) 244 | 245 | case wicore.DockingTop: 246 | _, height := child.View().NaturalSize() 247 | if height >= remaining.Height { 248 | height = remaining.Height 249 | } else if child.border != wicore.BorderNone { 250 | height++ 251 | } 252 | tmp := remaining 253 | tmp.Height = height 254 | remaining.Y += height 255 | remaining.Height -= height 256 | child.setRect(tmp) 257 | 258 | case wicore.DockingBottom: 259 | _, height := child.View().NaturalSize() 260 | if height >= remaining.Height { 261 | height = remaining.Height 262 | } else if child.border != wicore.BorderNone { 263 | height++ 264 | } 265 | tmp := remaining 266 | tmp.Y += (remaining.Height - height) 267 | tmp.Height = height 268 | remaining.Height -= height 269 | child.setRect(tmp) 270 | 271 | default: 272 | panic("Fill me") 273 | } 274 | } 275 | if fill != nil { 276 | fill.setRect(remaining) 277 | w.viewRect.X = 0 278 | w.viewRect.Y = 0 279 | w.viewRect.Width = 0 280 | w.viewRect.Height = 0 281 | w.view.SetSize(0, 0) 282 | } else { 283 | w.viewRect = remaining 284 | w.view.SetSize(w.viewRect.Width, w.viewRect.Height) 285 | } 286 | wicore.PostCommand(w.e, nil, "editor_redraw") 287 | } 288 | 289 | func (w *window) buffer() *raster.Buffer { 290 | // TODO(maruel): Redo API. 291 | // Opportunistically refresh the view buffer. 292 | if w.viewRect.Width != 0 && w.viewRect.Height != 0 { 293 | b := w.windowBuffer.SubBuffer(w.viewRect) 294 | b.Blit(w.view.Buffer()) 295 | } 296 | return w.windowBuffer 297 | } 298 | 299 | /* TODO(maruel): Is it needed at all? 300 | func (w *window) setView(view wicore.View) { 301 | if view != w.view { 302 | w.view = view 303 | b := w.windowBuffer.SubBuffer(w.viewRect) 304 | b.Fill(w.cell(' ')) 305 | wicore.PostCommand(w, "editor_redraw") 306 | } 307 | panic("To test") 308 | } 309 | */ 310 | 311 | // updateBorder calculates w.effectiveBorder, w.clientAreaRect and draws the 312 | // borders right away in the Window's buffer. 313 | // 314 | // It's called by setRect() and will be called by SetBorder (if ever 315 | // implemented). 316 | func (w *window) updateBorder() { 317 | if w.border == wicore.BorderNone { 318 | w.effectiveBorder = drawnBorderNone 319 | } else { 320 | w.effectiveBorder = calculateEffectiveBorder(w.rect, w.docking) 321 | } 322 | 323 | s := doubleBorder 324 | if w.border == wicore.BorderSingle { 325 | s = singleBorder 326 | } 327 | 328 | // TODO(maruel): Switch to a bitmask check by incrementally reducing w.clientAreaRect. 329 | switch w.effectiveBorder { 330 | case drawnBorderNone: 331 | w.clientAreaRect = raster.Rect{0, 0, w.rect.Width, w.rect.Height} 332 | 333 | case drawnBorderLeft: 334 | w.clientAreaRect = raster.Rect{1, 0, w.rect.Width - 1, w.rect.Height} 335 | w.windowBuffer.SubBuffer(raster.Rect{0, 0, 1, w.rect.Height}).Fill(w.cell(s[1])) 336 | 337 | case drawnBorderRight: 338 | w.clientAreaRect = raster.Rect{0, 0, w.rect.Width - 1, w.rect.Height} 339 | w.windowBuffer.SubBuffer(raster.Rect{w.rect.Width - 1, 0, 1, w.rect.Height}).Fill(w.cell(s[1])) 340 | 341 | case drawnBorderTop: 342 | w.clientAreaRect = raster.Rect{0, 1, w.rect.Width, w.rect.Height - 1} 343 | w.windowBuffer.SubBuffer(raster.Rect{0, 0, w.rect.Width, 1}).Fill(w.cell(s[0])) 344 | 345 | case drawnBorderBottom: 346 | w.clientAreaRect = raster.Rect{0, 0, w.rect.Width, w.rect.Height - 1} 347 | w.windowBuffer.SubBuffer(raster.Rect{0, w.rect.Height - 1, w.rect.Width, 1}).Fill(w.cell(s[0])) 348 | 349 | case drawnBorderAll: 350 | w.clientAreaRect = raster.Rect{1, 1, w.rect.Width - 2, w.rect.Height - 2} 351 | // Corners. 352 | *w.windowBuffer.Cell(0, 0) = w.cell(s[2]) 353 | *w.windowBuffer.Cell(0, w.rect.Height-1) = w.cell(s[4]) 354 | *w.windowBuffer.Cell(w.rect.Width-1, 0) = w.cell(s[3]) 355 | *w.windowBuffer.Cell(w.rect.Width-1, w.rect.Height-1) = w.cell(s[5]) 356 | // Lines. 357 | w.windowBuffer.SubBuffer(raster.Rect{1, 0, w.rect.Width - 2, 1}).Fill(w.cell(s[0])) 358 | w.windowBuffer.SubBuffer(raster.Rect{1, w.rect.Height - 1, w.rect.Width - 2, w.rect.Height - 1}).Fill(w.cell(s[0])) 359 | w.windowBuffer.SubBuffer(raster.Rect{0, 1, 1, w.rect.Height - 2}).Fill(w.cell(s[1])) 360 | w.windowBuffer.SubBuffer(raster.Rect{w.rect.Width - 1, 1, w.rect.Width - 1, w.rect.Height - 2}).Fill(w.cell(s[1])) 361 | 362 | default: 363 | panic("Unknown drawnBorder") 364 | } 365 | 366 | if w.clientAreaRect.Width < 0 { 367 | w.clientAreaRect.Width = 0 368 | panic("Fix this case") 369 | } 370 | if w.clientAreaRect.Height < 0 { 371 | w.clientAreaRect.Height = 0 372 | panic("Fix this case") 373 | } 374 | } 375 | 376 | func (w *window) getBorderFormat() raster.CellFormat { 377 | c := w.borderFormat 378 | if c.Empty() { 379 | // Defaults to the view format. 380 | c = w.view.DefaultFormat() 381 | if c.Empty() && w.parent != nil { 382 | // Defaults to the parent's format. 383 | c = w.parent.getBorderFormat() 384 | } 385 | } 386 | return c 387 | } 388 | 389 | func (w *window) cell(r rune) raster.Cell { 390 | return raster.Cell{r, w.getBorderFormat()} 391 | } 392 | 393 | func makeWindow(parent *window, view wicore.ViewW, docking wicore.DockingType) *window { 394 | log.Printf("makeWindow(%s, %s, %s)", parent, view.Title(), docking) 395 | var e wicore.Editor 396 | id := 0 397 | if parent != nil { 398 | e = parent.e 399 | parent.lastChildID++ 400 | id = parent.lastChildID 401 | } 402 | // It's more complex than that but it's a fine default. 403 | border := wicore.BorderNone 404 | if docking == wicore.DockingFloating { 405 | border = wicore.BorderDouble 406 | } 407 | return &window{ 408 | id: id, 409 | parent: parent, 410 | e: e, 411 | view: view, 412 | docking: docking, 413 | border: border, 414 | borderFormat: raster.CellFormat{ 415 | Fg: colors.White, 416 | Bg: colors.Black, 417 | }, 418 | } 419 | } 420 | 421 | // drawRecurse recursively draws the Window tree into buffer out. 422 | func drawRecurse(w *window, offsetX, offsetY int, out *raster.Buffer) { 423 | log.Printf("drawRecurse(%s, %d, %d); %v", w.View().Title(), offsetX, offsetY, w.Rect()) 424 | if w.Docking() == wicore.DockingFloating { 425 | // Floating Window are relative to the screen, not the parent Window. 426 | offsetX = 0 427 | offsetY = 0 428 | } 429 | // TODO(maruel): Only draw non-occuled Windows! 430 | dest := w.Rect() 431 | dest.X += offsetX 432 | dest.Y += offsetY 433 | out.SubBuffer(dest).Blit(w.buffer()) 434 | 435 | fillFound := false 436 | for _, child := range w.childrenWindows { 437 | // In the case of DockingFill, only the first one should be drawn. In 438 | // particular, the DockingFloating child of an hidden DockingFill will not 439 | // be drawn. 440 | if child.docking == wicore.DockingFill { 441 | if fillFound { 442 | continue 443 | } 444 | fillFound = true 445 | } 446 | drawRecurse(child, dest.X, dest.Y, out) 447 | } 448 | } 449 | 450 | // Commands 451 | 452 | func cmdWindowActivate(c *privilegedCommandImpl, e *editor, w *window, args ...string) { 453 | windowName := args[0] 454 | 455 | child := e.idToWindow(windowName) 456 | if child == nil { 457 | e.ExecuteCommand(w, "alert", isNotValidWindow.Formatf(windowName)) 458 | return 459 | } 460 | e.activateWindow(child) 461 | } 462 | 463 | func cmdWindowClose(c *privilegedCommandImpl, e *editor, w *window, args ...string) { 464 | windowName := args[0] 465 | 466 | child := e.idToWindow(windowName) 467 | if child == nil { 468 | e.ExecuteCommand(w, "alert", isNotValidWindow.Formatf(windowName)) 469 | return 470 | } 471 | for i, v := range child.parent.childrenWindows { 472 | if v == child { 473 | copy(w.childrenWindows[i:], w.childrenWindows[i+1:]) 474 | w.childrenWindows[len(w.childrenWindows)-1] = nil 475 | w.childrenWindows = w.childrenWindows[:len(w.childrenWindows)-1] 476 | detachRecursively(v) 477 | wicore.PostCommand(e, nil, "editor_redraw") 478 | return 479 | } 480 | } 481 | } 482 | 483 | func cmdWindowNew(c *privilegedCommandImpl, e *editor, w *window, args ...string) { 484 | windowName := args[0] 485 | dockingName := args[1] 486 | viewFactoryName := args[2] 487 | 488 | parent := e.idToWindow(windowName) 489 | if parent == nil { 490 | if viewFactoryName != "infobar_alert" { 491 | e.ExecuteCommand(w, "alert", isNotValidWindow.Formatf(windowName)) 492 | } 493 | return 494 | } 495 | 496 | docking := wicore.StringToDockingType(dockingName) 497 | if docking == wicore.DockingUnknown { 498 | if viewFactoryName != "infobar_alert" { 499 | e.ExecuteCommand(w, "alert", invalidDocking.Formatf(dockingName)) 500 | } 501 | return 502 | } 503 | // TODO(maruel): Only the first child Window with DockingFill is visible. 504 | // TODO(maruel): Reorder .childrenWindows with 505 | // CommandDispatcherFull.ActivateWindow() but only with DockingFill. 506 | // TODO(maruel): Also allow DockingFloating. 507 | //if docking != wicore.DockingFill { 508 | for _, child := range parent.childrenWindows { 509 | if child.Docking() == docking { 510 | if viewFactoryName != "infobar_alert" { 511 | e.ExecuteCommand(w, "alert", cantAddTwoWindowWithSameDocking.Formatf(docking)) 512 | } 513 | return 514 | } 515 | } 516 | //} 517 | 518 | viewFactory, ok := e.viewFactories[viewFactoryName] 519 | if !ok { 520 | if viewFactoryName != "infobar_alert" { 521 | e.ExecuteCommand(w, "alert", invalidViewFactory.Formatf(viewFactoryName)) 522 | } 523 | return 524 | } 525 | // TODO(maruel): e.nextViewID is an implementation detail, it's wrong. 526 | view := viewFactory(e, e.nextViewID, args[3:]...) 527 | e.nextViewID++ 528 | 529 | child := makeWindow(parent, view, docking) 530 | if docking == wicore.DockingFloating { 531 | width, height := view.NaturalSize() 532 | if child.border != wicore.BorderNone { 533 | width += 2 534 | height += 2 535 | } 536 | // TODO(maruel): Handle when width or height > scren size. 537 | // TODO(maruel): Not clean. Doesn't handle root Window resize properly. 538 | rootRect := e.rootWindow.Rect() 539 | child.rect.X = (rootRect.Width - width - 1) / 2 540 | child.rect.Y = (rootRect.Height - height - 1) / 2 541 | child.rect.Width = width 542 | child.rect.Height = height 543 | } 544 | parent.childrenWindows = append(parent.childrenWindows, child) 545 | parent.resizeChildren() 546 | // Call OnAttach() after the Window is attached to the parent. 547 | view.OnAttach(child) 548 | e.activateWindow(child) 549 | } 550 | 551 | func cmdWindowSetDocking(c *privilegedCommandImpl, e *editor, w *window, args ...string) { 552 | windowName := args[0] 553 | dockingName := args[1] 554 | 555 | child := e.idToWindow(windowName) 556 | if child == nil { 557 | e.ExecuteCommand(w, "alert", isNotValidWindow.Formatf(windowName)) 558 | return 559 | } 560 | docking := wicore.StringToDockingType(dockingName) 561 | if docking == wicore.DockingUnknown { 562 | e.ExecuteCommand(w, "alert", invalidDocking.Formatf(dockingName)) 563 | return 564 | } 565 | if w.docking != docking { 566 | // TODO(maruel): Check no other parent's child window have the same dock. 567 | w.docking = docking 568 | w.parent.resizeChildren() 569 | wicore.PostCommand(w.e, nil, "editor_redraw") 570 | } 571 | } 572 | 573 | func cmdWindowSetRect(c *privilegedCommandImpl, e *editor, w *window, args ...string) { 574 | windowName := args[0] 575 | 576 | child := e.idToWindow(windowName) 577 | if child == nil { 578 | e.ExecuteCommand(w, "alert", isNotValidWindow.Formatf(windowName)) 579 | return 580 | } 581 | r := raster.Rect{} 582 | var err1, err2, err3, err4 error 583 | r.X, err1 = strconv.Atoi(args[1]) 584 | r.Y, err2 = strconv.Atoi(args[2]) 585 | r.Width, err3 = strconv.Atoi(args[3]) 586 | r.Height, err4 = strconv.Atoi(args[4]) 587 | if err1 != nil || err2 != nil || err3 != nil || err4 != nil { 588 | e.ExecuteCommand(w, "alert", invalidRect.Formatf(args[1], args[2], args[3], args[4])) 589 | return 590 | } 591 | child.setRect(r) 592 | } 593 | 594 | // RegisterWindowCommands registers all the commands relative to window 595 | // management. 596 | func RegisterWindowCommands(dispatcher wicore.CommandsW) { 597 | cmds := []wicore.Command{ 598 | &privilegedCommandImpl{ 599 | "window_activate", 600 | 1, 601 | cmdWindowActivate, 602 | wicore.WindowCategory, 603 | lang.Map{ 604 | lang.En: "Activate a window", 605 | }, 606 | lang.Map{ 607 | lang.En: "Active a window. This means the Window will have keyboard focus.", 608 | }, 609 | }, 610 | &privilegedCommandImpl{ 611 | "window_close", 612 | 1, 613 | cmdWindowClose, 614 | wicore.WindowCategory, 615 | lang.Map{ 616 | lang.En: "Closes a window", 617 | }, 618 | lang.Map{ 619 | lang.En: "Closes a window. Note that any window can be closed and all the child window will be destroyed at the same time.", 620 | }, 621 | }, 622 | &privilegedCommandImpl{ 623 | "window_new", 624 | -1, 625 | cmdWindowNew, 626 | wicore.WindowCategory, 627 | lang.Map{ 628 | lang.En: "Creates a new window", 629 | }, 630 | lang.Map{ 631 | lang.En: "Usage: window_new \nCreates a new window. The new window is created as a child to the specified parent. It creates inside the window the view specified. The Window is activated. It is invalid to add a child Window with the same docking as one already present.", 632 | }, 633 | }, 634 | &privilegedCommandImpl{ 635 | "window_set_docking", 636 | 2, 637 | cmdWindowSetDocking, 638 | wicore.WindowCategory, 639 | lang.Map{ 640 | lang.En: "Change the docking of a window", 641 | }, 642 | lang.Map{ 643 | lang.En: "Changes the docking of this Window relative to the parent window. This will forces an invalidation and a redraw.", 644 | }, 645 | }, 646 | &privilegedCommandImpl{ 647 | "window_set_rect", 648 | 5, 649 | cmdWindowSetRect, 650 | wicore.WindowCategory, 651 | lang.Map{ 652 | lang.En: "Move a window", 653 | }, 654 | lang.Map{ 655 | lang.En: "Usage: window_set_rect \nMoves a Window relative to the parent window, unless it is floating, where it is relative to the view port.", 656 | }, 657 | }, 658 | } 659 | for _, cmd := range cmds { 660 | dispatcher.Register(cmd) 661 | } 662 | } 663 | -------------------------------------------------------------------------------- /internal/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Package internal contains the symbols that need to be exported so it can be 6 | // used in RPC via net/rpc but is not meant to be an API for end user plugins. 7 | // 8 | // It is based on https://golang.org/s/go14internal design principle. 9 | package internal 10 | -------------------------------------------------------------------------------- /internal/event_registry_internal.go: -------------------------------------------------------------------------------- 1 | // generated by go run ../tools/wi-event-generator/main.go ; DO NOT EDIT 2 | 3 | package internal 4 | 5 | import ( 6 | "github.com/wi-ed/wi/wicore" 7 | "github.com/wi-ed/wi/wicore/key" 8 | "github.com/wi-ed/wi/wicore/lang" 9 | ) 10 | 11 | // EventTriggerRPC is the low level interface to propagate events to plugins. 12 | // 13 | // It is implemented by wi/wicore/plugin, exported here to be used via RPC. 14 | type EventTriggerRPC interface { 15 | TriggerCommandsRPC(packet PacketCommands, ignored *int) error 16 | TriggerDocumentCreatedRPC(packet PacketDocumentCreated, ignored *int) error 17 | TriggerDocumentCursorMovedRPC(packet PacketDocumentCursorMoved, ignored *int) error 18 | TriggerEditorKeyboardModeChangedRPC(packet PacketEditorKeyboardModeChanged, ignored *int) error 19 | TriggerEditorLanguageRPC(packet PacketEditorLanguage, ignored *int) error 20 | TriggerTerminalKeyPressedRPC(packet PacketTerminalKeyPressed, ignored *int) error 21 | TriggerTerminalMetaKeyPressedRPC(packet PacketTerminalMetaKeyPressed, ignored *int) error 22 | TriggerTerminalResizedRPC(packet PacketTerminalResized, ignored *int) error 23 | TriggerViewActivatedRPC(packet PacketViewActivated, ignored *int) error 24 | TriggerViewCreatedRPC(packet PacketViewCreated, ignored *int) error 25 | TriggerWindowCreatedRPC(packet PacketWindowCreated, ignored *int) error 26 | TriggerWindowResizedRPC(packet PacketWindowResized, ignored *int) error 27 | } 28 | 29 | // PacketCommands is exported for internal RPC use. 30 | type PacketCommands struct { 31 | Cmds wicore.EnqueuedCommands 32 | } 33 | 34 | // PacketDocumentCreated is exported for internal RPC use. 35 | type PacketDocumentCreated struct { 36 | Doc wicore.Document 37 | } 38 | 39 | // PacketDocumentCursorMoved is exported for internal RPC use. 40 | type PacketDocumentCursorMoved struct { 41 | Doc wicore.Document 42 | Col int 43 | Row int 44 | } 45 | 46 | // PacketEditorKeyboardModeChanged is exported for internal RPC use. 47 | type PacketEditorKeyboardModeChanged struct { 48 | Mode wicore.KeyboardMode 49 | } 50 | 51 | // PacketEditorLanguage is exported for internal RPC use. 52 | type PacketEditorLanguage struct { 53 | L lang.Language 54 | } 55 | 56 | // PacketTerminalKeyPressed is exported for internal RPC use. 57 | type PacketTerminalKeyPressed struct { 58 | K key.Press 59 | } 60 | 61 | // PacketTerminalMetaKeyPressed is exported for internal RPC use. 62 | type PacketTerminalMetaKeyPressed struct { 63 | K key.Press 64 | } 65 | 66 | // PacketTerminalResized is exported for internal RPC use. 67 | type PacketTerminalResized struct { 68 | } 69 | 70 | // PacketViewActivated is exported for internal RPC use. 71 | type PacketViewActivated struct { 72 | View wicore.View 73 | } 74 | 75 | // PacketViewCreated is exported for internal RPC use. 76 | type PacketViewCreated struct { 77 | View wicore.View 78 | } 79 | 80 | // PacketWindowCreated is exported for internal RPC use. 81 | type PacketWindowCreated struct { 82 | Window wicore.Window 83 | } 84 | 85 | // PacketWindowResized is exported for internal RPC use. 86 | type PacketWindowResized struct { 87 | Window wicore.Window 88 | } 89 | -------------------------------------------------------------------------------- /internal/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | import ( 8 | "github.com/wi-ed/wi/wicore" 9 | "github.com/wi-ed/wi/wicore/lang" 10 | ) 11 | 12 | // PluginRPC is the low-level interface exposed by the plugin for use by 13 | // net/rpc. net/rpc forces the interface to be in a rigid format. 14 | type PluginRPC interface { 15 | // GetInfo is the fisrt function to be called synchronously. It must return 16 | // immediately. 17 | GetInfo(ignored lang.Language, out *wicore.PluginDetails) error 18 | // Init is called on plugin startup. All initialization should be done there. 19 | Init(in wicore.EditorDetails, ignored *int) error 20 | // Quit is called on editor termination. The editor waits for the function to 21 | // return. 22 | Quit(in int, ignored *int) error 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Package wi brings text based editor technology past 1200 bauds. 6 | // 7 | // This package contains only the non-unit-testable part of the editor. 8 | // 9 | // - editor/ contains the editor logic itself. It is terminal-agnostic. 10 | // - wicore/ contains the plugin glue. This module is shared by both the 11 | // editor itself and any wi-plugin-* for RPC. 12 | // - wi-plugin-sample/ is a sample plugin executable to `go install`. It is 13 | // both meant as a reusable skeleton to write a new plugin and as a way to 14 | // ensure the plugin system works. 15 | // 16 | // This project supports 'Debug' and 'Release' builds. The Release build is the 17 | // default, the Debug build has to be built explicitly. Use the following 18 | // command to generate a Debug build: 19 | // 20 | // go build -tags debug 21 | // 22 | // A debug build has additional functionalities: 23 | // 24 | // - Logs to wi.log. 25 | // - Has additional flags, for example it can create cpu profiles via 26 | // -cpuprofile and optionally serve profiling data over a builtin web server 27 | // at http://localhost:6060/debug/pprof via net/http/pprof with flag 28 | // -http=:6060. 29 | // - Has additional commands defined, see editor/debug.go for the list. 30 | // 31 | // Run "wi -h" for help about the additional flags after doing a Debug build. 32 | // 33 | // See README.md for more details. 34 | package main 35 | 36 | import ( 37 | "flag" 38 | "fmt" 39 | "os" 40 | 41 | "github.com/nsf/termbox-go" 42 | "github.com/wi-ed/wi/editor" 43 | "github.com/wi-ed/wi/wicore" 44 | ) 45 | 46 | func terminalThread(mustClose chan<- func()) int { 47 | // "flag" and "termbox" use a lot of global variables so they can't be easily 48 | // included in parallel tests. 49 | command := flag.Bool("c", false, "Runs the commands specified on startup") 50 | version := flag.Bool("v", false, "Prints version and exit") 51 | noPlugin := flag.Bool("no-plugin", false, "Disable loading plugins") 52 | flag.Parse() 53 | 54 | // Process this one early. No one wants version output to take 1s. 55 | if *version { 56 | println(version) 57 | return 0 58 | } 59 | 60 | if *command && flag.NArg() == 0 { 61 | fmt.Fprintf(os.Stderr, "error: -c implies specifying commands to execute") 62 | return 1 63 | } 64 | 65 | if err := termbox.Init(); err != nil { 66 | fmt.Fprintf(os.Stderr, "failed to initialize terminal: %s", err) 67 | return 1 68 | } 69 | 70 | out := debugHook() 71 | if out != nil { 72 | defer func() { 73 | _ = out.Close() 74 | }() 75 | } 76 | 77 | // It is really important that all other goroutine wrap with handlePanic(), 78 | // otherwise the terminal will be left in a broken state. 79 | mustClose <- termbox.Close 80 | termbox.SetInputMode(termbox.InputAlt | termbox.InputMouse) 81 | 82 | e, err := editor.MakeEditor(&TermBox{}, *noPlugin) 83 | if err != nil { 84 | fmt.Fprintf(os.Stderr, "error: %s", err) 85 | return 1 86 | } 87 | defer func() { 88 | _ = e.Close() 89 | }() 90 | debugHookEditor(e) 91 | 92 | wicore.PostCommand(e, nil, "editor_bootstrap_ui") 93 | if *command { 94 | for _, i := range flag.Args() { 95 | wicore.PostCommand(e, nil, i) 96 | } 97 | } else if flag.NArg() > 0 { 98 | for _, i := range flag.Args() { 99 | wicore.PostCommand(e, nil, "open", i) 100 | } 101 | } else { 102 | // If nothing, opens a blank editor. 103 | wicore.PostCommand(e, nil, "new") 104 | } 105 | return e.EventLoop() 106 | } 107 | 108 | func mainImpl() int { 109 | returnCode := make(chan int) 110 | var closer func() 111 | mustClose := make(chan func()) 112 | wicore.Go("terminalThread", func() { 113 | returnCode <- terminalThread(mustClose) 114 | }) 115 | for { 116 | select { 117 | case c := <-mustClose: 118 | closer = c 119 | case r := <-returnCode: 120 | if closer != nil { 121 | closer() 122 | } 123 | return r 124 | case p := <-wicore.GotPanic: 125 | fmt.Fprintf(os.Stderr, "Got panic!\n") 126 | if closer != nil { 127 | closer() 128 | } 129 | fmt.Fprintf(os.Stderr, "Panic: %s\n", p) 130 | return 1 131 | } 132 | } 133 | } 134 | 135 | func main() { 136 | os.Exit(mainImpl()) 137 | } 138 | -------------------------------------------------------------------------------- /non_debug.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // +build !debug 6 | 7 | package main 8 | 9 | import ( 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | 14 | "github.com/wi-ed/wi/editor" 15 | ) 16 | 17 | func debugHook() io.Closer { 18 | // It is important to get rid of log output on stderr as it would conflict 19 | // with the editor's use of the terminal. Sadly the strings are still 20 | // rasterized, I don't know of a way to get rid of this. 21 | log.SetFlags(0) 22 | log.SetOutput(ioutil.Discard) 23 | return nil 24 | } 25 | 26 | func debugHookEditor(e editor.Editor) { 27 | } 28 | -------------------------------------------------------------------------------- /terminal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // This file implements the conversion of editor.Terminal to termbox's. 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/nsf/termbox-go" 13 | "github.com/wi-ed/wi/editor" 14 | "github.com/wi-ed/wi/wicore" 15 | "github.com/wi-ed/wi/wicore/colors" 16 | "github.com/wi-ed/wi/wicore/key" 17 | "github.com/wi-ed/wi/wicore/raster" 18 | ) 19 | 20 | // TermBox implements the editor.Terminal interface that interacts with termbox. 21 | type TermBox struct { 22 | } 23 | 24 | // Size implements editor.Terminal. 25 | func (t TermBox) Size() (int, int) { 26 | return termbox.Size() 27 | } 28 | 29 | // SeedEvents implements editor.Terminal. 30 | func (t TermBox) SeedEvents() <-chan editor.TerminalEvent { 31 | // Converts termbox.Event into editor.TerminalEvent. This removes the need to 32 | // have an hard dependency of editor on termbox-go; this makes both unit 33 | // testing easier and future-proof the editor. 34 | c := make(chan editor.TerminalEvent) 35 | wicore.Go("SeedEvents", func() { 36 | for { 37 | e := termbox.PollEvent() 38 | switch e.Type { 39 | case termbox.EventKey: 40 | // TODO(maruel): Key translation. 41 | c <- editor.TerminalEvent{ 42 | Type: editor.EventKey, 43 | Key: termboxKeyToKeyPress(e), 44 | } 45 | case termbox.EventResize: 46 | c <- editor.TerminalEvent{ 47 | Type: editor.EventKey, 48 | Size: editor.Size{Width: e.Width, Height: e.Height}, 49 | } 50 | case termbox.EventError: 51 | close(c) 52 | return 53 | } 54 | } 55 | }) 56 | return c 57 | } 58 | 59 | // termboxKeyToKeyPress returns the key.Press compatible event. 60 | func termboxKeyToKeyPress(k termbox.Event) key.Press { 61 | out := key.Press{} 62 | if k.Mod&termbox.ModAlt != 0 { 63 | out.Alt = true 64 | } 65 | switch termbox.Key(k.Key) { 66 | case termbox.KeyF1: 67 | out.Key = key.F1 68 | case termbox.KeyF2: 69 | out.Key = key.F2 70 | case termbox.KeyF3: 71 | out.Key = key.F3 72 | case termbox.KeyF4: 73 | out.Key = key.F4 74 | case termbox.KeyF5: 75 | out.Key = key.F5 76 | case termbox.KeyF6: 77 | out.Key = key.F6 78 | case termbox.KeyF7: 79 | out.Key = key.F7 80 | case termbox.KeyF8: 81 | out.Key = key.F8 82 | case termbox.KeyF9: 83 | out.Key = key.F9 84 | case termbox.KeyF10: 85 | out.Key = key.F10 86 | case termbox.KeyF11: 87 | out.Key = key.F11 88 | case termbox.KeyF12: 89 | out.Key = key.F12 90 | case termbox.KeyInsert: 91 | out.Key = key.Insert 92 | case termbox.KeyDelete: 93 | out.Key = key.Delete 94 | case termbox.KeyHome: 95 | out.Key = key.Home 96 | case termbox.KeyEnd: 97 | out.Key = key.End 98 | case termbox.KeyPgup: 99 | out.Key = key.PageUp 100 | case termbox.KeyPgdn: 101 | out.Key = key.PageDown 102 | case termbox.KeyArrowUp: 103 | out.Key = key.Up 104 | case termbox.KeyArrowDown: 105 | out.Key = key.Down 106 | case termbox.KeyArrowLeft: 107 | out.Key = key.Left 108 | case termbox.KeyArrowRight: 109 | out.Key = key.Right 110 | 111 | case termbox.KeyCtrlSpace: // KeyCtrlTilde, KeyCtrl2 112 | // This value is 0, which cannot be distinguished from non-keypress. 113 | if k.Ch == 0 { 114 | out.Ctrl = true 115 | out.Key = key.Space 116 | } else { 117 | // Normal keypress code path. 118 | out.Ch = k.Ch 119 | } 120 | 121 | case termbox.KeyCtrlA: 122 | out.Ctrl = true 123 | out.Ch = 'a' 124 | case termbox.KeyCtrlB: 125 | out.Ctrl = true 126 | out.Ch = 'b' 127 | case termbox.KeyCtrlC: 128 | out.Ctrl = true 129 | out.Ch = 'c' 130 | case termbox.KeyCtrlD: 131 | out.Ctrl = true 132 | out.Ch = 'd' 133 | case termbox.KeyCtrlE: 134 | out.Ctrl = true 135 | out.Ch = 'e' 136 | case termbox.KeyCtrlF: 137 | out.Ctrl = true 138 | out.Ch = 'f' 139 | case termbox.KeyCtrlG: 140 | out.Ctrl = true 141 | out.Ch = 'g' 142 | case termbox.KeyBackspace: // KeyCtrlH 143 | case termbox.KeyBackspace2: 144 | out.Key = key.Backspace 145 | case termbox.KeyTab: // KeyCtrlI 146 | out.Key = key.Tab 147 | case termbox.KeyCtrlJ: 148 | out.Ctrl = true 149 | out.Ch = 'j' 150 | case termbox.KeyCtrlK: 151 | out.Ctrl = true 152 | out.Ch = 'k' 153 | case termbox.KeyCtrlL: 154 | out.Ctrl = true 155 | out.Ch = 'l' 156 | case termbox.KeyEnter: // KeyCtrlM 157 | out.Key = key.Enter 158 | case termbox.KeyCtrlN: 159 | out.Ctrl = true 160 | out.Ch = 'n' 161 | case termbox.KeyCtrlO: 162 | out.Ctrl = true 163 | out.Ch = 'o' 164 | case termbox.KeyCtrlP: 165 | out.Ctrl = true 166 | out.Ch = 'p' 167 | case termbox.KeyCtrlQ: 168 | out.Ctrl = true 169 | out.Ch = 'q' 170 | case termbox.KeyCtrlR: 171 | out.Ctrl = true 172 | out.Ch = 'r' 173 | case termbox.KeyCtrlS: 174 | out.Ctrl = true 175 | out.Ch = 's' 176 | case termbox.KeyCtrlT: 177 | out.Ctrl = true 178 | out.Ch = 't' 179 | case termbox.KeyCtrlU: 180 | out.Ctrl = true 181 | out.Ch = 'u' 182 | case termbox.KeyCtrlV: 183 | out.Ctrl = true 184 | out.Ch = 'v' 185 | case termbox.KeyCtrlW: 186 | out.Ctrl = true 187 | out.Ch = 'w' 188 | case termbox.KeyCtrlX: 189 | out.Ctrl = true 190 | out.Ch = 'x' 191 | case termbox.KeyCtrlY: 192 | out.Ctrl = true 193 | out.Ch = 'y' 194 | case termbox.KeyCtrlZ: 195 | out.Ctrl = true 196 | out.Ch = 'z' 197 | case termbox.KeyEsc: // KeyCtrlLsqBracket, KeyCtrl3 198 | out.Key = key.Escape 199 | case termbox.KeyCtrl4: // KeyCtrlBackslash 200 | out.Ctrl = true 201 | out.Ch = '4' 202 | case termbox.KeyCtrl5: // KeyCtrlRsqBracket 203 | out.Ctrl = true 204 | out.Ch = '5' 205 | case termbox.KeyCtrl6: 206 | out.Ctrl = true 207 | out.Ch = '6' 208 | case termbox.KeyCtrl7: // KeyCtrlSlash, KeyCtrlUnderscore 209 | out.Ctrl = true 210 | out.Ch = '7' 211 | case termbox.KeySpace: 212 | out.Key = key.Space 213 | default: 214 | panic(fmt.Sprintf("Unhandled key %x", k.Key)) 215 | } 216 | return out 217 | } 218 | 219 | // Converts a RGB color into the nearest termbox color. 220 | func rgbToTermBox(c colors.RGB) termbox.Attribute { 221 | switch colors.NearestEGA(c) { 222 | case colors.Black: 223 | return termbox.ColorBlack 224 | case colors.Blue: 225 | return termbox.ColorBlue 226 | case colors.Green: 227 | return termbox.ColorGreen 228 | case colors.Cyan: 229 | return termbox.ColorCyan 230 | case colors.Red: 231 | return termbox.ColorRed 232 | case colors.Magenta: 233 | return termbox.ColorMagenta 234 | case colors.Brown: 235 | return termbox.ColorYellow 236 | case colors.LightGray: 237 | return termbox.ColorWhite 238 | case colors.DarkGray: 239 | return termbox.ColorBlack | termbox.AttrBold 240 | case colors.BrightBlue: 241 | return termbox.ColorBlue | termbox.AttrBold 242 | case colors.BrightGreen: 243 | return termbox.ColorGreen | termbox.AttrBold 244 | case colors.BrightCyan: 245 | return termbox.ColorCyan | termbox.AttrBold 246 | case colors.BrightRed: 247 | return termbox.ColorRed | termbox.AttrBold 248 | case colors.BrightMagenta: 249 | return termbox.ColorMagenta | termbox.AttrBold 250 | case colors.BrightYellow: 251 | return termbox.ColorYellow | termbox.AttrBold 252 | case colors.White: 253 | return termbox.ColorWhite | termbox.AttrBold 254 | default: 255 | return termbox.ColorDefault 256 | } 257 | } 258 | 259 | // Blit converts the editor.Buffer format into termbox format. 260 | func (t TermBox) Blit(b *raster.Buffer) { 261 | width, height := termbox.Size() 262 | cells := termbox.CellBuffer() 263 | if width > b.Width { 264 | width = b.Width 265 | } 266 | if height > b.Height { 267 | height = b.Height 268 | } 269 | 270 | for y := 0; y < height; y++ { 271 | for x := 0; x < width; x++ { 272 | i := y*width + x 273 | cell := b.Cell(x, y) 274 | cells[i].Ch = cell.R 275 | cells[i].Fg = rgbToTermBox(cell.F.Fg) 276 | // TODO(maruel): Not sure. 277 | if cell.F.Underline { 278 | cells[i].Fg |= termbox.AttrUnderline 279 | } 280 | cells[i].Bg = rgbToTermBox(cell.F.Bg) 281 | // TODO(maruel): Not sure. Some terminal may cause Bg&Bold to be Blinking. 282 | if cell.F.Italic { 283 | cells[i].Bg |= termbox.AttrUnderline 284 | } 285 | } 286 | } 287 | if err := termbox.Flush(); err != nil { 288 | panic(err) 289 | } 290 | } 291 | 292 | // SetCursor moves the terminal cursor. 293 | func (t TermBox) SetCursor(col, line int) { 294 | termbox.SetCursor(col, line) 295 | } 296 | -------------------------------------------------------------------------------- /tools/print_colors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2014 Marc-Antoine Ruel. All rights reserved. 3 | # Use of this source code is governed under the Apache License, Version 2.0 4 | # that can be found in the LICENSE file. 5 | 6 | # Prints all 256 colors in a terminal. 7 | 8 | reset=$(tput op) 9 | y=$(printf %$((${COLUMNS}-6))s) 10 | for i in {0..255}; do 11 | index=00$i 12 | echo -e ${index:${#index}-3:3} `tput setaf $i;tput setab $i`${y// /=}$reset 13 | done 14 | -------------------------------------------------------------------------------- /tools/test.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: Copyright 2014 Marc-Antoine Ruel. All rights reserved. 3 | :: Use of this source code is governed under the Apache License, Version 2.0 4 | :: that can be found in the LICENSE file. 5 | 6 | setlocal 7 | 8 | :: Short test until we got something up and running. 9 | cd %~dp0\.. 10 | cls 11 | go build -race -tags debug 12 | if errorlevel 1 goto :EOF 13 | set WIPLUGINSPATH=. 14 | wi -c log_all editor_quit 15 | if errorlevel 1 goto :EOF 16 | type wi.log 17 | -------------------------------------------------------------------------------- /tools/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2014 Marc-Antoine Ruel. All rights reserved. 3 | # Use of this source code is governed under the Apache License, Version 2.0 4 | # that can be found in the LICENSE file. 5 | 6 | # Short test until we got something up and running. 7 | 8 | set -e 9 | 10 | cd $(dirname $0)/.. 11 | go build -race -tags debug 12 | WIPLUGINSPATH=. ./wi -c log_all editor_quit 13 | cat wi.log 14 | -------------------------------------------------------------------------------- /tools/wi-event-generator/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Generates editor/event_registry.go from wicore/events.go. 6 | package main 7 | 8 | import ( 9 | "bytes" 10 | "flag" 11 | "fmt" 12 | "go/parser" 13 | "go/token" 14 | "io/ioutil" 15 | "os" 16 | "path/filepath" 17 | "sort" 18 | "strings" 19 | "text/template" 20 | 21 | "github.com/wi-ed/wi/tools/wi-event-generator/parse" 22 | ) 23 | 24 | var tmplDecl = template.Must(template.New("decl").Parse(`// generated by {{.CmdLine}}; DO NOT EDIT 25 | 26 | package {{.CurrentPkg}} 27 | 28 | import ( 29 | "io" 30 | 31 | "github.com/wi-ed/wi/wicore/key" 32 | "github.com/wi-ed/wi/wicore/lang" 33 | ) 34 | 35 | // EventListener is to be used to cancel an event listener. 36 | type EventListener interface { 37 | io.Closer 38 | } 39 | 40 | // NumberEvents is the number of known events. 41 | const NumberEvents = {{len .Events}} 42 | 43 | // EventRegistry permits to register callbacks that are called on events. 44 | // 45 | // Warning: This interface is automatically generated. 46 | type EventRegistry interface { 47 | EventTrigger 48 | 49 | {{range .Events}} 50 | Register{{.Name}}(callback func({{.Params.Flat $.CurrentPkg}})) EventListener{{end}} 51 | } 52 | `)) 53 | 54 | var tmplInternal = template.Must(template.New("internal").Parse(`// generated by {{.CmdLine}}; DO NOT EDIT 55 | 56 | package {{.CurrentPkg}} 57 | 58 | import ( 59 | "github.com/wi-ed/wi/wicore" 60 | "github.com/wi-ed/wi/wicore/key" 61 | "github.com/wi-ed/wi/wicore/lang" 62 | ) 63 | 64 | // EventTriggerRPC is the low level interface to propagate events to plugins. 65 | // 66 | // It is implemented by wi/wicore/plugin, exported here to be used via RPC. 67 | type EventTriggerRPC interface { 68 | {{range .Events}} 69 | Trigger{{.Name}}RPC(packet Packet{{.Name}}, ignored *int) error{{end}} 70 | } 71 | 72 | {{range .Events}} 73 | // Packet{{.Name}} is exported for internal RPC use. 74 | type Packet{{.Name}} struct { 75 | {{range .ParamsUpperCase}} {{.Name}} {{.FullType $.CurrentPkg}} 76 | {{end}} } 77 | {{end}} 78 | `)) 79 | 80 | // commonImpl is the common implementation code between the editor and the 81 | // plugin. For now it is prefered to duplicate it than export it. That could 82 | // change if an 'internal' package is created. 83 | // TODO(maruel): Create a template that is imported. 84 | var commonImpl = ` 85 | {{range .Events}} 86 | type listener{{.Name}} struct { 87 | id int 88 | callback func({{.Params.Flat $.CurrentPkg}}) 89 | } 90 | {{end}} 91 | // eventRegistry is automatically generated via wi-event-generator from the 92 | // interface wicore.EventRegistry. It completely implements 93 | // wicore.EventRegistry. 94 | type eventRegistry struct { 95 | lock sync.Mutex 96 | nextID int 97 | deferred chan<- func() 98 | {{range .Events}} 99 | {{.Lower}} []listener{{.Name}}{{end}} 100 | } 101 | 102 | func (er *eventRegistry) unregister(eventID int) { 103 | er.lock.Lock() 104 | defer er.lock.Unlock() 105 | // TODO(maruel): The buffers are never reallocated, so it's effectively a 106 | // memory leak. 107 | switch(eventID & {{.BitMask}}) { {{range .Events}} 108 | case {{.BitValue}}: 109 | for index, value := range er.{{.Lower}} { 110 | if value.id == eventID { 111 | copy(er.{{.Lower}}[index:], er.{{.Lower}}[index+1:]) 112 | er.{{.Lower}} = er.{{.Lower}}[0 : len(er.{{.Lower}})-1] 113 | return 114 | } 115 | }{{end}} 116 | } 117 | }{{range .Events}} 118 | 119 | func (er *eventRegistry) Register{{.Name}}(callback func({{.Params.Flat $.CurrentPkg}})) wicore.EventListener { 120 | er.lock.Lock() 121 | defer er.lock.Unlock() 122 | i := er.nextID 123 | er.nextID++ 124 | er.{{.Lower}} = append(er.{{.Lower}}, listener{{.Name}}{i, callback}) 125 | return &eventListener{er, i | {{.BitValue}}} 126 | }{{end}}{{range .Events}} 127 | 128 | func (er *eventRegistry) {{$.Trigger}}{{.Name}}({{.Params.Flat $.CurrentPkg}}) { 129 | er.deferred <- func() { 130 | items := func() []func({{.Params.Flat $.CurrentPkg}}) { 131 | er.lock.Lock() 132 | defer er.lock.Unlock() 133 | items := make([]func({{.Params.Flat $.CurrentPkg}}), 0, len(er.{{.Lower}})) 134 | for _, item := range er.{{.Lower}} { 135 | items = append(items, item.callback) 136 | } 137 | return items 138 | }() 139 | for _, item := range items { 140 | item({{.Params.Names}}) 141 | } 142 | } 143 | }{{end}} 144 | 145 | type unregister interface { 146 | unregister(id int) 147 | } 148 | 149 | type eventListener struct { 150 | unregister unregister 151 | id int 152 | } 153 | 154 | func (e *eventListener) Close() error { 155 | if e.id == 0 { 156 | return errors.New("EventListener already closed") 157 | } 158 | e.unregister.unregister(e.id) 159 | e.id = 0 160 | return nil 161 | } 162 | ` 163 | 164 | var tmplEditorImpl = template.Must(template.New("editor").Parse(`// generated by {{.CmdLine}}; DO NOT EDIT 165 | 166 | package {{.CurrentPkg}} 167 | 168 | import ( 169 | "errors" 170 | "log" 171 | "net/rpc" 172 | "sync" 173 | 174 | "github.com/wi-ed/wi/internal" 175 | "github.com/wi-ed/wi/wicore" 176 | "github.com/wi-ed/wi/wicore/key" 177 | "github.com/wi-ed/wi/wicore/lang" 178 | ) 179 | 180 | // makeEventRegistry returns a wicore.EventRegistry and the channel to read 181 | // from to run the events piped in. 182 | func makeEventRegistry() (wicore.EventRegistry, chan func()) { 183 | // Reduce the odds of allocation within RegistryXXX() by using relatively 184 | // large buffers. 185 | c := make(chan func(), 2048) 186 | e := &eventRegistry{ 187 | deferred: c,{{range .Events}} 188 | {{.Lower}}: make([]listener{{.Name}}, 0, 64),{{end}} 189 | } 190 | return e, c 191 | } 192 | 193 | // registerPluginEvents registers all the events to be forwarded to the plugin 194 | // through the interface EventTriggerRPC. 195 | func registerPluginEvents(client *rpc.Client, e wicore.EventRegistry) wicore.EventListener { 196 | return wicore.MultiCloser{ {{range .Events}} 197 | e.Register{{.Name}}(func({{.Params.Flat $.CurrentPkg}}) { 198 | packet := internal.Packet{{.Name}} { {{.Params.Names}} } 199 | out := 0 200 | if err := client.Call("EventTriggerRPC.Trigger{{.Name}}RPC", packet, &out); err != nil { 201 | log.Printf("RPC {{.Name}} call failure: %s", err) 202 | } 203 | }),{{end}} 204 | } 205 | } 206 | ` + commonImpl)) 207 | 208 | var tmplPluginImpl = template.Must(template.New("plugin").Parse(`// generated by {{.CmdLine}}; DO NOT EDIT 209 | 210 | package {{.CurrentPkg}} 211 | 212 | import ( 213 | "errors" 214 | "sync" 215 | 216 | "github.com/wi-ed/wi/internal" 217 | "github.com/wi-ed/wi/wicore" 218 | "github.com/wi-ed/wi/wicore/key" 219 | "github.com/wi-ed/wi/wicore/lang" 220 | ) 221 | 222 | type eventTriggerRPC struct { 223 | eventRegistry 224 | } 225 | 226 | // makeEventRegistry returns a wicore.EventRegistry and the channel to read 227 | // from to run the events piped in. 228 | func makeEventRegistry() (wicore.EventRegistry, internal.EventTriggerRPC, chan func()) { 229 | // Reduce the odds of allocation within RegistryXXX() by using relatively 230 | // large buffers. 231 | c := make(chan func(), 2048) 232 | e := &eventTriggerRPC{ 233 | eventRegistry{ 234 | deferred: c,{{range .Events}} 235 | {{.Lower}}: make([]listener{{.Name}}, 0, 64),{{end}} 236 | }, 237 | } 238 | return e, e, c 239 | }{{range .Events}} 240 | 241 | func (er *eventTriggerRPC) Trigger{{.Name}}RPC(packet internal.Packet{{.Name}}, ignored *int) error { 242 | er.trigger{{.Name}}({{.PacketParams}}) 243 | return nil 244 | }{{end}}{{range .Events}} 245 | 246 | func (er *eventRegistry) Trigger{{.Name}}({{.Params.Flat $.CurrentPkg}}) { 247 | // TODO(maruel): Send it upstream to the editor. 248 | }{{end}} 249 | ` + commonImpl)) 250 | 251 | // Event describes one parsed event from the interface that will be used to 252 | // generate the code. 253 | type Event struct { 254 | Name string 255 | Lower string 256 | Index int 257 | BitValue string 258 | Params parse.Args // []Arg{{"foo", "int"}, {"bar", "string"}} 259 | ParamsUpperCase parse.Args // []Arg{{"Foo", "int"}, {"Bar", "string"}} 260 | PacketParams string // "packet.foo, packet.bar" 261 | } 262 | 263 | type tmplData struct { 264 | BitMask string 265 | CmdLine string 266 | CurrentPkg string 267 | Events []Event 268 | Trigger string // lowercase if not exported, in case of plugin. 269 | } 270 | 271 | // Generation code. 272 | 273 | func extractEvents(inputFile, inputType string, bitmask uint) ([]Event, error) { 274 | fset := token.NewFileSet() 275 | f, err := parser.ParseFile(fset, inputFile, nil, 0) 276 | if err != nil { 277 | return nil, err 278 | } 279 | prefix := "Trigger" 280 | events := []Event{} 281 | inputT := parse.FindType(f, inputType) 282 | if inputT == nil { 283 | return nil, fmt.Errorf("failed to find type %s", inputType) 284 | } 285 | pkgName := f.Name.Name 286 | methods, err := parse.EnumInterface(pkgName, inputT) 287 | if err != nil { 288 | return nil, err 289 | } 290 | methodNames := make([]string, len(methods)) 291 | for i, m := range methods { 292 | methodNames[i] = m.Name 293 | } 294 | if !sort.StringsAreSorted(methodNames) { 295 | return nil, fmt.Errorf("methods of %s must be sorted by name", inputType) 296 | } 297 | for _, method := range methods { 298 | if !strings.HasPrefix(method.Name, prefix) { 299 | return nil, fmt.Errorf("method %s.%s doesn't have prefix %s", inputType, method.Name, prefix) 300 | } 301 | if len(method.Results) != 0 { 302 | return nil, fmt.Errorf("unexpected result on method %s.%s", inputType, method.Name) 303 | } 304 | name := method.Name[len(prefix):] 305 | lower := strings.ToLower(name[0:1]) + name[1:] 306 | paramsUpper := make(parse.Args, len(method.Params)) 307 | packetParams := make([]string, len(method.Params)) 308 | for i, arg := range method.Params { 309 | if len(arg.Name) == 0 { 310 | return nil, fmt.Errorf("argument %d must be named on method %s.%s", i, inputType, method.Name) 311 | } 312 | paramsUpper[i] = arg 313 | paramsUpper[i].Name = strings.ToUpper(paramsUpper[i].Name[0:1]) + paramsUpper[i].Name[1:] 314 | packetParams[i] = "packet." + paramsUpper[i].Name 315 | } 316 | // TODO(maruel): It creates an artificial limit of 2^23 event listener and 317 | // 2^8 event types on 32 bits systems. 318 | events = append(events, Event{ 319 | Name: name, 320 | Lower: lower, 321 | Index: len(events), 322 | BitValue: fmt.Sprintf("0x%x", (len(events)+1)<= 0, the command will be aborted if the number of arguments is not exactly this value. Set to -1 to disable verification. On abort, an alert with the long description of the command is done. 22 | HandlerValue CommandImplHandler 23 | CategoryValue CommandCategory 24 | ShortDescValue lang.Map 25 | LongDescValue lang.Map 26 | } 27 | 28 | // Name implements Command. 29 | func (c *CommandImpl) Name() string { 30 | return c.NameValue 31 | } 32 | 33 | // Handle implements Command. 34 | func (c *CommandImpl) Handle(e EditorW, w Window, args ...string) { 35 | if c.ExpectedArgs != -1 && len(args) != c.ExpectedArgs { 36 | e.ExecuteCommand(w, "alert", c.LongDesc()) 37 | } 38 | c.HandlerValue(c, e, w, args...) 39 | } 40 | 41 | // Category implements Command. 42 | func (c *CommandImpl) Category(e Editor, w Window) CommandCategory { 43 | return c.CategoryValue 44 | } 45 | 46 | // ShortDesc implements Command. 47 | func (c *CommandImpl) ShortDesc() string { 48 | return c.ShortDescValue.String() 49 | } 50 | 51 | // LongDesc implements Command. 52 | func (c *CommandImpl) LongDesc() string { 53 | return c.LongDescValue.String() 54 | } 55 | 56 | // CommandAlias references another command by its name. It's important to not 57 | // bind directly to the Command reference, so that if a command is replaced by 58 | // a plugin, that the replacement command is properly called by the alias. 59 | type CommandAlias struct { 60 | NameValue string 61 | CommandValue string 62 | ArgsValue []string 63 | } 64 | 65 | // Name implements Command. 66 | func (c *CommandAlias) Name() string { 67 | return c.NameValue 68 | } 69 | 70 | // Handle implements Command. 71 | func (c *CommandAlias) Handle(e EditorW, w Window, args ...string) { 72 | // The alias is executed inline. This is important for command queue 73 | // ordering. 74 | cmd := GetCommand(e, w, c.CommandValue) 75 | if cmd != nil { 76 | cmd.Handle(e, w, args...) 77 | } else { 78 | // TODO(maruel): This makes assumption on "alert". 79 | cmd = GetCommand(e, w, "alert") 80 | txt := AliasNotFound.Formatf(c.NameValue, c.CommandValue) 81 | cmd.Handle(e, w, txt) 82 | } 83 | } 84 | 85 | // Category implements Command. 86 | func (c *CommandAlias) Category(e Editor, w Window) CommandCategory { 87 | cmd := GetCommand(e, w, c.CommandValue) 88 | if cmd != nil { 89 | return c.Category(e, w) 90 | } 91 | return UnknownCategory 92 | } 93 | 94 | // ShortDesc implements Command. 95 | func (c *CommandAlias) ShortDesc() string { 96 | return AliasFor.Formatf(c.merged()) 97 | } 98 | 99 | // LongDesc implements Command. 100 | func (c *CommandAlias) LongDesc() string { 101 | return AliasFor.Formatf(c.merged()) 102 | } 103 | 104 | func (c *CommandAlias) merged() string { 105 | out := c.CommandValue 106 | if len(c.ArgsValue) != 0 { 107 | out += " " + strings.Join(c.ArgsValue, " ") 108 | } 109 | return out 110 | } 111 | 112 | // Utility functions. 113 | 114 | // PostCommand appends a Command at the end of the queue. It is a shortcut to 115 | // e.TriggerCommands(EnqueuedCommands{...}). 116 | func PostCommand(e EventRegistry, callback func(), cmdName string, args ...string) { 117 | line := make([]string, len(args)+1) 118 | line[0] = cmdName 119 | copy(line[1:], args) 120 | e.TriggerCommands(EnqueuedCommands{[][]string{line}, callback}) 121 | } 122 | 123 | // GetCommand traverses the Window hierarchy tree to find a View that has 124 | // the command cmd in its Commands mapping. If Window is nil, it starts with 125 | // the Editor's active Window. 126 | func GetCommand(e Editor, w Window, cmdName string) Command { 127 | if w == nil { 128 | w = e.ActiveWindow() 129 | } 130 | for { 131 | cmd := w.View().Commands().Get(cmdName) 132 | if cmd != nil { 133 | return cmd 134 | } 135 | w = w.Parent() 136 | if w == nil { 137 | return nil 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /wicore/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Package wicore implements all the interfaces to be shared between the wi 6 | // editor process and its plugins. 7 | // 8 | // It is strongly versioned via the SHA-1 of its interface. 9 | // 10 | // Overview graph of the Editor and its hierarchical tree. In addition to 11 | // knowing all Window as a tree, it has a direct link to every documents. This 12 | // is because a document buffer may be loaded but not have any view associated, 13 | // or two Views (panels) may be viewing the same Document. 14 | // 15 | // +------+ 16 | // |Editor|-------------+--------+ 17 | // +------+ | | 18 | // | | v 19 | // v | +--------+ 20 | // +------+ | |Document| 21 | // |Window| | +--------+ 22 | // +------+ | 23 | // | | 24 | // +-----+-------+ | 25 | // | | | 26 | // v v | 27 | // +------+ +------+ | 28 | // |Window| |Window| | 29 | // +------+ +------+ | 30 | // | | 31 | // v | 32 | // +----+ | 33 | // |View| | 34 | // +----+ | 35 | // | | 36 | // v | 37 | // +--------+ | 38 | // |Document|<----+ 39 | // +--------+ 40 | // 41 | // 42 | // A View references Commands specific to this View, keyboard mapping specific 43 | // to this View and potentially a Document. Many View do not have a Document 44 | // associated, like Status, the Command View, etc. 45 | // 46 | // +----+ 47 | // |View| 48 | // +----+ 49 | // | 50 | // +--------+---+------------+ 51 | // | | | 52 | // v v v 53 | // +--------+ +-----------+ +--------+ 54 | // |Commands| |KeyBindings| |Document| 55 | // +--------+ +-----------+ +--------+ 56 | // | 57 | // | +-------+ 58 | // +-->|Command| 59 | // | +-------+ 60 | // | 61 | // | +-------+ 62 | // +-->|Command| 63 | // +-------+ 64 | // 65 | // Objects living exclusively in wi main process: 66 | // 67 | // - Editor 68 | // - Window 69 | // 70 | // Objects that can be in either in the wi main process or implemented (and 71 | // brokered to the main process) in a plugin process implement the Proxyable 72 | // interface. 73 | package wicore 74 | -------------------------------------------------------------------------------- /wicore/event_registry_decl.go: -------------------------------------------------------------------------------- 1 | // generated by go run ../tools/wi-event-generator/main.go ; DO NOT EDIT 2 | 3 | package wicore 4 | 5 | import ( 6 | "io" 7 | 8 | "github.com/wi-ed/wi/wicore/key" 9 | "github.com/wi-ed/wi/wicore/lang" 10 | ) 11 | 12 | // EventListener is to be used to cancel an event listener. 13 | type EventListener interface { 14 | io.Closer 15 | } 16 | 17 | // NumberEvents is the number of known events. 18 | const NumberEvents = 12 19 | 20 | // EventRegistry permits to register callbacks that are called on events. 21 | // 22 | // Warning: This interface is automatically generated. 23 | type EventRegistry interface { 24 | EventTrigger 25 | 26 | RegisterCommands(callback func(cmds EnqueuedCommands)) EventListener 27 | RegisterDocumentCreated(callback func(doc Document)) EventListener 28 | RegisterDocumentCursorMoved(callback func(doc Document, col, row int)) EventListener 29 | RegisterEditorKeyboardModeChanged(callback func(mode KeyboardMode)) EventListener 30 | RegisterEditorLanguage(callback func(l lang.Language)) EventListener 31 | RegisterTerminalKeyPressed(callback func(k key.Press)) EventListener 32 | RegisterTerminalMetaKeyPressed(callback func(k key.Press)) EventListener 33 | RegisterTerminalResized(callback func()) EventListener 34 | RegisterViewActivated(callback func(view View)) EventListener 35 | RegisterViewCreated(callback func(view View)) EventListener 36 | RegisterWindowCreated(callback func(window Window)) EventListener 37 | RegisterWindowResized(callback func(window Window)) EventListener 38 | } 39 | -------------------------------------------------------------------------------- /wicore/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // This file defines all the code to be used by the wi editor and to be 6 | // accessable by plugins. 7 | //go:generate go run ../tools/wi-event-generator/main.go 8 | 9 | // "stringer" can be installed with "go get golang.org/x/tools/cmd/stringer" 10 | //go:generate stringer -output=interfaces_string.go -type=BorderType,CommandCategory,DockingType,KeyboardMode 11 | 12 | package wicore 13 | 14 | import ( 15 | "fmt" 16 | "io" 17 | "strings" 18 | 19 | "github.com/wi-ed/wi/wicore/key" 20 | "github.com/wi-ed/wi/wicore/lang" 21 | "github.com/wi-ed/wi/wicore/raster" 22 | ) 23 | 24 | // UI 25 | 26 | // DockingType defines the relative position of a Window relative to its parent. 27 | type DockingType int 28 | 29 | // Available docking options. 30 | const ( 31 | // DockingUnknown is an invalid value. 32 | DockingUnknown DockingType = iota 33 | 34 | // DockingFill means the Window uses all the available space. 35 | 36 | DockingFill 37 | // DockingFloating means the Window is not constrained by the parent window 38 | // size and location. 39 | DockingFloating 40 | 41 | DockingLeft 42 | DockingRight 43 | DockingTop 44 | DockingBottom 45 | ) 46 | 47 | // StringToDockingType converts a string back to a DockingType. 48 | func StringToDockingType(s string) DockingType { 49 | switch s { 50 | case "fill": 51 | return DockingFill 52 | case "floating": 53 | return DockingFloating 54 | case "left": 55 | return DockingLeft 56 | case "right": 57 | return DockingRight 58 | case "top": 59 | return DockingTop 60 | case "bottom": 61 | return DockingBottom 62 | default: 63 | return DockingUnknown 64 | } 65 | } 66 | 67 | // BorderType defines the type of border for a Window. 68 | type BorderType int 69 | 70 | const ( 71 | // BorderNone means width is 0. 72 | BorderNone BorderType = iota 73 | // BorderSingle means width is 1. 74 | BorderSingle 75 | // BorderDouble means width is 1 despite its name, only the glyph is 76 | // different. 77 | BorderDouble 78 | ) 79 | 80 | // Proxyable represents an object that can be proxied to a plugin. 81 | type Proxyable interface { 82 | // ID represents the unique object id. The IDs must be unique through the 83 | // process lifetime of the editor. 84 | ID() string 85 | } 86 | 87 | // EventTrigger declares the valid events that can be triggered. 88 | // 89 | // Do not use this interface directly, use the automatically-generated 90 | // interface EventRegistry instead. 91 | type EventTrigger interface { 92 | // TriggerCommands dispatches one or multiple commands to the current active 93 | // listener. Normally, it's the View contained to the active Window. Using 94 | // this function guarantees that all the commands will be executed in order 95 | // without commands interfering. 96 | // 97 | // `callback` is called synchronously after the command is executed. 98 | TriggerCommands(cmds EnqueuedCommands) 99 | TriggerDocumentCreated(doc Document) 100 | TriggerDocumentCursorMoved(doc Document, col, row int) 101 | TriggerEditorKeyboardModeChanged(mode KeyboardMode) 102 | TriggerEditorLanguage(l lang.Language) 103 | TriggerTerminalKeyPressed(k key.Press) 104 | TriggerTerminalMetaKeyPressed(k key.Press) 105 | TriggerTerminalResized() 106 | TriggerViewActivated(view View) 107 | TriggerViewCreated(view View) 108 | TriggerWindowCreated(window Window) 109 | TriggerWindowResized(window Window) 110 | } 111 | 112 | // Editor is the output device and the main process context. It shows the root 113 | // window which covers the whole screen estate. 114 | type Editor interface { 115 | Proxyable 116 | EventRegistry 117 | 118 | // ActiveWindow returns the current active Window. 119 | ActiveWindow() Window 120 | // ViewFactoryNames return the name of all the view factories. 121 | ViewFactoryNames() []string 122 | // AllDocuments returns all the active documents. Some of them may not be in 123 | // a View. 124 | AllDocuments() []Document 125 | // AllPlugins returns details about all the loaded plugins. 126 | AllPlugins() []PluginDetails 127 | // KeyboardMode is global to the editor. It matches vim behavior. For example 128 | // in a 2-window setup while in insert mode, using Ctrl-O, Ctrl-W, Down will 129 | // move to the next window but will stay in insert mode. 130 | // 131 | // Technically, each View could have their own KeyboardMode but in practice 132 | // it just creates a cognitive overhead without much benefit. 133 | KeyboardMode() KeyboardMode 134 | // Version returns the version number of this build of wi. 135 | Version() string 136 | } 137 | 138 | // EditorW is the writable version of Editor. 139 | type EditorW interface { 140 | Editor 141 | 142 | // ExecuteCommand executes a command now. This is only meant to run a command 143 | // reentrantly; e.g. running a command triggers another one. This usually 144 | // happens for key binding, command aliases, when a command triggers an error. 145 | // 146 | // TODO(maruel): Remove? 147 | ExecuteCommand(w Window, cmdName string, args ...string) 148 | // RegisterViewFactory makes a new view available by name. 149 | RegisterViewFactory(name string, viewFactory ViewFactory) bool 150 | } 151 | 152 | // Window is a View container. It defines the position, Z-ordering via 153 | // hierarchy and decoration. It can have multiple child windows. The child 154 | // windows are not bounded by the parent window if DockingFloating is used. The 155 | // Window itself doesn't interact with the user, since it only has a non-client 156 | // area (the border). All the client area is covered by the View. 157 | // 158 | // Split view is not supported. A 4-way merge setup can be created with the 159 | // following Window setup as 4 child Window of the root Window: 160 | // 161 | // +-----------+-----------+------------+ 162 | // | Remote |Merge Base*| Local | 163 | // |DockingLeft|DockingFill|DockingRight| 164 | // | | | | 165 | // +-----------+-----------+------------+ 166 | // | Result | 167 | // | DockingBottom | 168 | // | | 169 | // +------------------------------------+ 170 | // 171 | // * The Merge Base View can be either: 172 | // - The root Window's View that is constained. 173 | // - A child Window set as DockingFill. In this case, the root Window View is 174 | // not visible. 175 | // 176 | // The end result is that this use case doesn't require any "split" support. 177 | // Further subdivision can be done via Window containment. 178 | // 179 | // The Window interface exists for synchronous query but modifications 180 | // (creation, closing, moving) are done asynchronously via commands. A set of 181 | // privileged commands starting with the prefix "window_" can modify Window 182 | // instances, designating the actual Window by its .ID() method. 183 | type Window interface { 184 | fmt.Stringer 185 | Proxyable 186 | 187 | // Parent returns the parent Window. 188 | Parent() Window 189 | // ChildrenWindows returns a copy of the slice of children windows. 190 | ChildrenWindows() []Window 191 | // Rect returns the position based on the parent Window area, except if 192 | // Docking() is DockingFloating. 193 | Rect() raster.Rect 194 | // Docking returns where this Window is docked relative to the parent Window. 195 | // A DockingFloating window is effectively starting a new independent Rect. 196 | Docking() DockingType 197 | // View returns the View contained by this Window. There is exactly one. 198 | View() View 199 | } 200 | 201 | // View is content presented in a Window. For example it can be a TextBuffer or 202 | // a command box. View define the key binding and commands supported so it 203 | // responds to user input. 204 | type View interface { 205 | fmt.Stringer 206 | io.Closer 207 | Proxyable 208 | 209 | // Commands returns the commands registered for this specific view. For 210 | // example a text window will have commands specific to the file type 211 | // enabled. 212 | Commands() Commands 213 | // KeyBindings returns the key bindings registered for this specific key. For 214 | // example the 'command' view has different behavior on up/down arrow keys 215 | // than a text editor view. 216 | KeyBindings() KeyBindings 217 | // Title is View's title, which can be the current file name or any other 218 | // relevant detail. 219 | Title() string 220 | // IsDisabled returns false if the View can be activated to receive user 221 | // inputs at all. 222 | IsDisabled() bool 223 | // Buffer returns the display buffer for this Window. 224 | Buffer() *raster.Buffer 225 | // NaturalSize returns the natural size of the content. It can be -1 for as 226 | // long/large as possible, 0 if indeterminate. The return value of this 227 | // function is not affected by SetSize(). 228 | NaturalSize() (width, height int) 229 | // DefaultFormat returns the default coloring for this View. If this View has 230 | // an CellFormat.Empty()==true format, it will uses whatever parent Window's 231 | // View DefaultFormat(). 232 | DefaultFormat() raster.CellFormat 233 | } 234 | 235 | // ViewW is the writable version of View. 236 | type ViewW interface { 237 | View 238 | 239 | // CommandsW returns the writeable interface of Commands. 240 | CommandsW() CommandsW 241 | // KeyBindingsW returns the writeable interface of KeyBindings. 242 | KeyBindingsW() KeyBindingsW 243 | // SetSize resets the View Buffer size. 244 | SetSize(x, y int) 245 | // OnAttach is called by the Window after it was attached. 246 | // TODO(maruel): Maybe split in ViewFull? 247 | OnAttach(w Window) 248 | } 249 | 250 | // ViewFactory returns a new View. 251 | type ViewFactory func(e Editor, id int, args ...string) ViewW 252 | 253 | // Document represents an open document. It can be accessed by zero, one or 254 | // multiple View. For example the document may not be visible at all as a 'back 255 | // buffer', may be loaded in a View or in multiple View, each having their own 256 | // coloring and cursor position. 257 | type Document interface { 258 | fmt.Stringer 259 | io.Closer 260 | Proxyable 261 | 262 | // RenderInto renders a view of a document. 263 | // 264 | // TODO(maruel): Likely return a new Buffer instance instead, for RPC 265 | // friendlyness. To be decided. 266 | RenderInto(buffer *raster.Buffer, view View, offsetColumn, offsetLine int) 267 | // FileType returns the file type as determined by the scanners. 268 | FileType() FileType 269 | // IsDirty is true if the content should be saved before quitting. 270 | IsDirty() bool 271 | } 272 | 273 | // CommandCategory is used to put commands into sections for help purposes. 274 | type CommandCategory int 275 | 276 | const ( 277 | // UnknownCategory means the command couldn't be categorized. 278 | UnknownCategory CommandCategory = iota 279 | // WindowCategory are commands relating to manipuling windows and UI in 280 | // general. 281 | WindowCategory 282 | // CommandsCategory are commands relating to manipulating commands, aliases, 283 | // keybindings. 284 | CommandsCategory 285 | // EditorCategory are commands relating to the editor lifetime. 286 | EditorCategory 287 | // DebugCategory are commands relating to debugging the app itself or plugins. 288 | DebugCategory 289 | 290 | // TODO(maruel): Add other categories. 291 | ) 292 | 293 | // CommandHandler executes the command cmd on the Window w. 294 | type CommandHandler func(e EditorW, w Window, args ...string) 295 | 296 | // Command describes a registered command that can be triggered directly at the 297 | // command prompt, via a keybinding or a plugin. 298 | // 299 | // A Command is immutable once created either by the editor process or by a 300 | // plugin. 301 | type Command interface { 302 | // Name is the name of the command. 303 | Name() string 304 | // Handle executes the command. 305 | Handle(e EditorW, w Window, args ...string) 306 | // Category returns the category the command should be bucketed in, for help 307 | // documentation purpose. 308 | Category(e Editor, w Window) CommandCategory 309 | // ShortDesc returns a short description of the command in the language 310 | // requested. 311 | ShortDesc() string 312 | // LongDesc returns a long explanation of the command in the language 313 | // requested. 314 | LongDesc() string 315 | } 316 | 317 | // Commands stores the known commands. This is where plugins can add new 318 | // commands. Each View contains its own Commands. 319 | type Commands interface { 320 | // Get returns a command if registered, nil otherwise. 321 | Get(cmdName string) Command 322 | // GetNames() return the name of all the commands. 323 | GetNames() []string 324 | } 325 | 326 | // CommandsW is the writable version of Commands. 327 | type CommandsW interface { 328 | Commands 329 | 330 | // Register registers a command so it can be executed later. In practice 331 | // commands should normally be registered on startup. Returns false if a 332 | // command was already registered and was lost. 333 | Register(cmd Command) bool 334 | } 335 | 336 | // EnqueuedCommands is used internally to dispatch commands through 337 | // EventRegistry. 338 | type EnqueuedCommands struct { 339 | Commands [][]string 340 | Callback func() 341 | } 342 | 343 | // KeyboardMode defines the keyboard mapping (input mode) to use. 344 | // 345 | // Unlike vim, there's no Command-line and Ex modes. It's unnecessary because 346 | // the command window is a Window on its own, instead of a additional input 347 | // mode on the current Window. 348 | // 349 | // TODO(maruel): vim also has visual/select which will be necessary. 350 | type KeyboardMode int 351 | 352 | const ( 353 | // Normal is the mode where typing letters results in commands, not 354 | // content editing. 355 | Normal KeyboardMode = iota + 1 356 | // Insert is the mode where typing letters results in content, not 357 | // commands. 358 | Insert 359 | // AllMode is to bind keys independent of the current mode. It is useful for 360 | // function keys, Ctrl-, arrow keys, etc. 361 | AllMode 362 | ) 363 | 364 | // KeyBindings stores the mapping between keyboard entry and commands. This 365 | // includes what can be considered "macros" as much as casual things like arrow 366 | // keys. 367 | // 368 | // TODO(maruel): Right now there's two ways to add bindings, either through 369 | // calls or through commands. Prefer one over the other. 370 | type KeyBindings interface { 371 | // Get returns a command if registered, nil otherwise. 372 | Get(mode KeyboardMode, key key.Press) string 373 | // GetAssigned returns all the assigned keys for this mode. 374 | GetAssigned(mode KeyboardMode) []key.Press 375 | } 376 | 377 | // KeyBindingsW is the writable version of KeyBindings. 378 | type KeyBindingsW interface { 379 | KeyBindings 380 | 381 | // Set registers a keyboard mapping. In practice keyboard mappings 382 | // should normally be registered on startup. Returns false if a key mapping 383 | // was already registered and was lost. Set cmdName to "" to remove a key 384 | // binding. 385 | Set(mode KeyboardMode, key key.Press, cmdName string) bool 386 | } 387 | 388 | // EditorDetails is sent over the wire to plugins. 389 | type EditorDetails struct { 390 | ID string 391 | Version string 392 | } 393 | 394 | // PluginDetails is details for a plugin. 395 | type PluginDetails struct { 396 | Name string 397 | Description string 398 | } 399 | 400 | // Plugin is a simplified interface that represents a live plugin process. 401 | // It's the high level object. 402 | // 403 | // Communication flow goes this way: 404 | // Editor -> Plugin -> internal.PluginRPC -> net/rpc -> -> net/rpc -> internal.PluginRPC -> Plugin 405 | // 406 | // The Plugin implementation in the editor process is a stub. Execution 407 | // eventually flows up to the plugin process' Plugin instance. 408 | type Plugin interface { 409 | io.Closer 410 | fmt.Stringer 411 | 412 | // Details returns the plugin details for UI purposes. It's the first 413 | // function called and it is called synchronously. wicore/plugin provides a 414 | // trivial implementation. 415 | Details() PluginDetails 416 | 417 | // Init is called to do slower initialization part and is called 418 | // asynchronously as the editor process is started. When this function is 419 | // called, events are already registered and can fire simultaneously. It's up 420 | // to the plugin to handle these events properly. 421 | Init(e Editor) 422 | 423 | // TODO(maruel): Split InitSync() + InitAsync() when needed to force 424 | // synchronous (fast part) then asynchronous (slow delayed part) 425 | // initialization. 426 | } 427 | 428 | // FileType is the type as determined by the scanner. It uses a hierarchical 429 | // categorization of file formats. 430 | type FileType string 431 | 432 | // New types can safely be defined by a plugin. 433 | const ( 434 | Scanning = FileType("Scanning") 435 | Code = FileType("Code") // All files that can be considered "source code" in its broadest meaning. 436 | CodeCFamily = FileType("Code.C") // C covers all C derivatives. 437 | CodeCC = FileType("Code.C.C") 438 | CodeCCSource = FileType("Code.C.C.Source") 439 | CodeCCHeader = FileType("Code.C.C.Header") 440 | CodeCCPP = FileType("Code.C.C++") 441 | CodeCCPPSource = FileType("Code.C.C++.Source") 442 | CodeCCPPHeader = FileType("Code.C.C++.Header") 443 | CodeGo = FileType("Code.Go") 444 | ) 445 | 446 | // Base returns the base file type for this file type 447 | func (f FileType) Base() FileType { 448 | return FileType(strings.SplitN(string(f), ".", 2)[0]) 449 | } 450 | -------------------------------------------------------------------------------- /wicore/interfaces_string.go: -------------------------------------------------------------------------------- 1 | // generated by stringer -output=interfaces_string.go -type=BorderType,CommandCategory,DockingType,KeyboardMode; DO NOT EDIT 2 | 3 | package wicore 4 | 5 | import "fmt" 6 | 7 | const _BorderType_name = "BorderNoneBorderSingleBorderDouble" 8 | 9 | var _BorderType_index = [...]uint8{0, 10, 22, 34} 10 | 11 | func (i BorderType) String() string { 12 | if i < 0 || i+1 >= BorderType(len(_BorderType_index)) { 13 | return fmt.Sprintf("BorderType(%d)", i) 14 | } 15 | return _BorderType_name[_BorderType_index[i]:_BorderType_index[i+1]] 16 | } 17 | 18 | const _CommandCategory_name = "UnknownCategoryWindowCategoryCommandsCategoryEditorCategoryDebugCategory" 19 | 20 | var _CommandCategory_index = [...]uint8{0, 15, 29, 45, 59, 72} 21 | 22 | func (i CommandCategory) String() string { 23 | if i < 0 || i+1 >= CommandCategory(len(_CommandCategory_index)) { 24 | return fmt.Sprintf("CommandCategory(%d)", i) 25 | } 26 | return _CommandCategory_name[_CommandCategory_index[i]:_CommandCategory_index[i+1]] 27 | } 28 | 29 | const _DockingType_name = "DockingUnknownDockingFillDockingFloatingDockingLeftDockingRightDockingTopDockingBottom" 30 | 31 | var _DockingType_index = [...]uint8{0, 14, 25, 40, 51, 63, 73, 86} 32 | 33 | func (i DockingType) String() string { 34 | if i < 0 || i+1 >= DockingType(len(_DockingType_index)) { 35 | return fmt.Sprintf("DockingType(%d)", i) 36 | } 37 | return _DockingType_name[_DockingType_index[i]:_DockingType_index[i+1]] 38 | } 39 | 40 | const _KeyboardMode_name = "NormalInsertAllMode" 41 | 42 | var _KeyboardMode_index = [...]uint8{0, 6, 12, 19} 43 | 44 | func (i KeyboardMode) String() string { 45 | i -= 1 46 | if i < 0 || i+1 >= KeyboardMode(len(_KeyboardMode_index)) { 47 | return fmt.Sprintf("KeyboardMode(%d)", i+1) 48 | } 49 | return _KeyboardMode_name[_KeyboardMode_index[i]:_KeyboardMode_index[i+1]] 50 | } 51 | -------------------------------------------------------------------------------- /wicore/key/key.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | //go:generate stringer -type=Key 6 | 7 | // Package key implements generic key definition. 8 | package key 9 | 10 | import "strings" 11 | 12 | // Key represents a non-character key. 13 | type Key int 14 | 15 | // Known non-character keys. 16 | // 17 | // Keys between None and Meta are not meta keys as they can be represented with 18 | // a character, e.g. \n, \i, ' ', \t. 19 | const ( 20 | None Key = iota 21 | Enter 22 | Escape 23 | Space 24 | Tab 25 | Meta 26 | F1 27 | F2 28 | F3 29 | F4 30 | F5 31 | F6 32 | F7 33 | F8 34 | F9 35 | F10 36 | F11 37 | F12 38 | F13 39 | F14 40 | F15 41 | Backspace 42 | Delete 43 | Insert 44 | Home 45 | End 46 | PageUp 47 | PageDown 48 | Up 49 | Down 50 | Left 51 | Right 52 | last 53 | ) 54 | 55 | // StringToKey parses the string presentation of a key back into a Key. 56 | // 57 | // Returns None on invalid key name. 58 | func StringToKey(key string) Key { 59 | switch key { 60 | case "Escape": 61 | return Escape 62 | case "Enter": 63 | return Enter 64 | case "Space": 65 | return Space 66 | case "Tab": 67 | return Tab 68 | case "Meta": 69 | return Meta 70 | case "F1": 71 | return F1 72 | case "F2": 73 | return F2 74 | case "F3": 75 | return F3 76 | case "F4": 77 | return F4 78 | case "F5": 79 | return F5 80 | case "F6": 81 | return F6 82 | case "F7": 83 | return F7 84 | case "F8": 85 | return F8 86 | case "F9": 87 | return F9 88 | case "F10": 89 | return F10 90 | case "F11": 91 | return F11 92 | case "F12": 93 | return F12 94 | case "F13": 95 | return F13 96 | case "F14": 97 | return F14 98 | case "F15": 99 | return F15 100 | case "Backspace": 101 | return Backspace 102 | case "Delete": 103 | return Delete 104 | case "Insert": 105 | return Insert 106 | case "Home": 107 | return Home 108 | case "End": 109 | return End 110 | case "PageUp": 111 | return PageUp 112 | case "PageDown": 113 | return PageDown 114 | case "Up": 115 | return Up 116 | case "Down": 117 | return Down 118 | case "Left": 119 | return Left 120 | case "Right": 121 | return Right 122 | default: 123 | return None 124 | } 125 | } 126 | 127 | // Press represents a key press. 128 | // 129 | // Only one of Key or Ch is set. 130 | type Press struct { 131 | Alt bool 132 | Ctrl bool 133 | Key Key // Non-character key (e.g. F-keys, arrows, tab, space, etc). Set to None when not used. 134 | Ch rune // Character key, e.g. letter, number. Set to rune(0) when not used. 135 | } 136 | 137 | func (k Press) String() string { 138 | if k.Key == None && k.Ch == 0 { 139 | return "" 140 | } 141 | out := "" 142 | if k.Ctrl { 143 | out += "Ctrl-" 144 | } 145 | if k.Alt { 146 | out += "Alt-" 147 | } 148 | if k.Key > None && k.Key < last { 149 | out += k.Key.String() 150 | } else { 151 | out += string(k.Ch) 152 | } 153 | return out 154 | } 155 | 156 | // IsMeta returns true if a key press is a meta key. This also includes 157 | // characters with Ctrl or Alt held. 158 | func (k Press) IsMeta() bool { 159 | return k.Alt || k.Ctrl || k.Key >= Meta 160 | } 161 | 162 | // IsValid returns true if the object represents a key press. 163 | func (k Press) IsValid() bool { 164 | return k.Alt || k.Ctrl || k.Key != None || k.Ch != rune(0) 165 | } 166 | 167 | // StringToPress parses a string and returns a Press. 168 | func StringToPress(keyName string) Press { 169 | out := Press{} 170 | if strings.HasPrefix(keyName, "Ctrl-") { 171 | keyName = keyName[5:] 172 | out.Ctrl = true 173 | } 174 | if strings.HasPrefix(keyName, "Alt-") { 175 | keyName = keyName[4:] 176 | out.Alt = true 177 | } 178 | rest := []rune(keyName) 179 | l := len(rest) 180 | if l == 1 { 181 | out.Ch = rest[0] 182 | } else if l > 1 { 183 | out.Key = StringToKey(keyName) 184 | } 185 | return out 186 | } 187 | -------------------------------------------------------------------------------- /wicore/key/key_string.go: -------------------------------------------------------------------------------- 1 | // generated by stringer -type=Key; DO NOT EDIT 2 | 3 | package key 4 | 5 | import "fmt" 6 | 7 | const _Key_name = "NoneEnterEscapeSpaceTabMetaF1F2F3F4F5F6F7F8F9F10F11F12F13F14F15BackspaceDeleteInsertHomeEndPageUpPageDownUpDownLeftRightlast" 8 | 9 | var _Key_index = [...]uint8{0, 4, 9, 15, 20, 23, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 48, 51, 54, 57, 60, 63, 72, 78, 84, 88, 91, 97, 105, 107, 111, 115, 120, 124} 10 | 11 | func (i Key) String() string { 12 | if i < 0 || i+1 >= Key(len(_Key_index)) { 13 | return fmt.Sprintf("Key(%d)", i) 14 | } 15 | return _Key_name[_Key_index[i]:_Key_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /wicore/key/key_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package key 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/maruel/ut" 11 | ) 12 | 13 | func TestKey(t *testing.T) { 14 | for i := None; i < last; i++ { 15 | s := i.String() 16 | ut.AssertEqual(t, true, len(s) > 1) 17 | ut.AssertEqual(t, i, StringToKey(s)) 18 | } 19 | 20 | ut.AssertEqual(t, "Key(33)", Key(last+1).String()) 21 | } 22 | 23 | func TestPress(t *testing.T) { 24 | data := []Press{ 25 | {false, false, None, 'a'}, 26 | {false, false, None, 'A'}, 27 | {false, false, None, 'é'}, 28 | {false, false, Space, '\000'}, 29 | {false, false, Tab, '\000'}, 30 | {false, false, F1, '\000'}, 31 | } 32 | for i, v := range data { 33 | ut.AssertEqualIndex(t, i, true, v.IsValid()) 34 | v.Ctrl = true 35 | ut.AssertEqualIndex(t, i, v, StringToPress(v.String())) 36 | v.Alt = true 37 | ut.AssertEqualIndex(t, i, v, StringToPress(v.String())) 38 | v.Ctrl = false 39 | ut.AssertEqualIndex(t, i, v, StringToPress(v.String())) 40 | } 41 | 42 | ut.AssertEqual(t, "", Press{}.String()) 43 | 44 | for i := Key(0); i < Meta; i++ { 45 | ut.AssertEqual(t, false, Press{Key: i}.IsMeta()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /wicore/lang/lang.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Package lang handles localization of language UI. 6 | package lang 7 | 8 | import ( 9 | "fmt" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | var lock sync.Mutex 15 | var active = En 16 | 17 | // Language is used to declare the language for UI purpose. 18 | type Language string 19 | 20 | // Known languages. 21 | // 22 | // TODO(maruel): Add new languages when translating the application. 23 | const ( 24 | En Language = "en" 25 | Es Language = "es" 26 | Fr Language = "fr" 27 | FrCa Language = "fr_ca" 28 | ) 29 | 30 | // Map is the mapping of strings based on the language. 31 | type Map map[Language]string 32 | 33 | // Get returns the string for the language if present, defaults to En. 34 | func (m Map) Get(lang Language) string { 35 | s, ok := m[lang] 36 | if !ok { 37 | items := strings.Split(string(lang), "_") 38 | s, ok = m[Language(items[0])] 39 | if !ok { 40 | s, ok = m[En] 41 | } 42 | } 43 | return s 44 | } 45 | 46 | func (m Map) String() string { 47 | return m.Get(Active()) 48 | } 49 | 50 | // Formatf returns fmt.Sprintf(m.String(), args...) as a shortcut. 51 | func (m Map) Formatf(args ...interface{}) string { 52 | return fmt.Sprintf(m.String(), args...) 53 | } 54 | 55 | // Active returns the active language for the process. 56 | func Active() Language { 57 | lock.Lock() 58 | defer lock.Unlock() 59 | return active 60 | } 61 | 62 | // Set sets the active language for the process. 63 | func Set(lang Language) { 64 | lock.Lock() 65 | defer lock.Unlock() 66 | active = lang 67 | } 68 | -------------------------------------------------------------------------------- /wicore/lang/lang_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package lang 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/maruel/ut" 11 | ) 12 | 13 | func TestGetDefaultEn(t *testing.T) { 14 | a := Map{ 15 | En: "Foo", 16 | FrCa: "Bar", 17 | } 18 | ut.AssertEqual(t, "Foo", a.Get(Es)) 19 | } 20 | 21 | func TestGetDefaultNonCountry(t *testing.T) { 22 | a := Map{ 23 | Fr: "Bar", 24 | } 25 | ut.AssertEqual(t, "Bar", a.Get(FrCa)) 26 | } 27 | 28 | func TestGetDefaultMissing(t *testing.T) { 29 | a := Map{ 30 | FrCa: "Bar", 31 | } 32 | ut.AssertEqual(t, "", a.Get(Es)) 33 | } 34 | 35 | func TestString(t *testing.T) { 36 | a := Map{ 37 | FrCa: "Bar", 38 | } 39 | Set(FrCa) 40 | ut.AssertEqual(t, "Bar", a.String()) 41 | } 42 | 43 | func TestFormatf(t *testing.T) { 44 | a := Map{ 45 | FrCa: "Bar %d", 46 | } 47 | Set(FrCa) 48 | ut.AssertEqual(t, "Bar 2", a.Formatf(2)) 49 | } 50 | -------------------------------------------------------------------------------- /wicore/panic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package wicore 6 | 7 | import ( 8 | "fmt" 9 | "runtime" 10 | ) 11 | 12 | // GotPanic must be listened to in the main thread to know if a panic() call 13 | // occured in any goroutine running under Go(). 14 | var GotPanic <-chan interface{} 15 | 16 | var gotPanic chan<- interface{} 17 | 18 | func init() { 19 | p := make(chan interface{}) 20 | GotPanic = p 21 | gotPanic = p 22 | } 23 | 24 | // Go wraps a call to wrap any panic() call and pipe it to the main goroutine. 25 | // 26 | // This permits clean shutdown, like terminal cleanup or plugin closure, when 27 | // the program crashes. 28 | func Go(name string, f func()) { 29 | go func() { 30 | defer func() { 31 | if i := recover(); i != nil { 32 | // TODO(maruel): No allocation in recover handling. 33 | buf := make([]byte, 2048) 34 | n := runtime.Stack(buf, false) 35 | if n != 0 { 36 | buf = buf[:n-1] 37 | } 38 | gotPanic <- fmt.Errorf("%s panicked: %s\nStack: %s", name, i, buf) 39 | } 40 | }() 41 | f() 42 | }() 43 | } 44 | -------------------------------------------------------------------------------- /wicore/plugin/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Package plugin implements the common code to implement a wi plugin. 6 | package plugin 7 | 8 | import ( 9 | "fmt" 10 | "io" 11 | "log" 12 | "net/rpc" 13 | "os" 14 | 15 | "github.com/wi-ed/wi/internal" 16 | "github.com/wi-ed/wi/wicore" 17 | "github.com/wi-ed/wi/wicore/lang" 18 | ) 19 | 20 | // Impl is the base implementation of interface wicore.Plugin. Embed this 21 | // structure and override the functions desired. 22 | type Impl struct { 23 | Name string 24 | Description lang.Map 25 | } 26 | 27 | func (i *Impl) String() string { 28 | return fmt.Sprintf("Plugin(%s, %d)", i.Name, os.Getpid()) 29 | } 30 | 31 | // Details implements wicore.Plugin. 32 | func (i *Impl) Details() wicore.PluginDetails { 33 | return wicore.PluginDetails{ 34 | i.Name, 35 | i.Description.String(), 36 | } 37 | } 38 | 39 | // Init implements wicore.Plugin. 40 | func (i *Impl) Init(e wicore.Editor) { 41 | } 42 | 43 | // Close implements wicore.Plugin. 44 | func (i *Impl) Close() error { 45 | return nil 46 | } 47 | 48 | // pluginRPC implements internal.PluginRPC and implement common bookeeping. 49 | type pluginRPC struct { 50 | conn io.Closer 51 | langListener wicore.EventListener 52 | plugin wicore.Plugin 53 | e *editorProxy 54 | } 55 | 56 | func (p *pluginRPC) GetInfo(l lang.Language, out *wicore.PluginDetails) error { 57 | lang.Set(l) 58 | *out = p.plugin.Details() 59 | return nil 60 | } 61 | 62 | func (p *pluginRPC) Init(details wicore.EditorDetails, ignored *int) error { 63 | p.e.id = details.ID 64 | p.e.version = details.Version 65 | p.langListener = p.e.RegisterEditorLanguage(func(l lang.Language) { 66 | // Propagate the information. 67 | lang.Set(l) 68 | }) 69 | p.plugin.Init(p.e) 70 | return nil 71 | } 72 | 73 | func (p *pluginRPC) Quit(int, *int) error { 74 | // TODO(maruel): Is it really worth cancelling event listeners? It's just 75 | // unnecessary slow down, we should favor performance in the shutdown code. 76 | if p.langListener != nil { 77 | _ = p.langListener.Close() 78 | p.langListener = nil 79 | } 80 | p.e = nil 81 | err := p.plugin.Close() 82 | if p.conn != nil { 83 | _ = p.conn.Close() 84 | p.conn = nil 85 | } 86 | return err 87 | } 88 | 89 | // editorProxy is an experimentation. 90 | type editorProxy struct { 91 | wicore.EventRegistry 92 | deferred chan func() 93 | id string 94 | activeWindow wicore.Window 95 | factoryNames []string 96 | keyboardMode wicore.KeyboardMode 97 | version string 98 | } 99 | 100 | func (e *editorProxy) ID() string { 101 | return e.id 102 | } 103 | 104 | func (e *editorProxy) ActiveWindow() wicore.Window { 105 | return e.activeWindow 106 | } 107 | 108 | func (e *editorProxy) ViewFactoryNames() []string { 109 | out := make([]string, len(e.factoryNames)) 110 | for i, v := range e.factoryNames { 111 | out[i] = v 112 | } 113 | return out 114 | } 115 | 116 | func (e *editorProxy) AllDocuments() []wicore.Document { 117 | return nil 118 | } 119 | 120 | func (e *editorProxy) AllPlugins() []wicore.PluginDetails { 121 | return nil 122 | } 123 | 124 | func (e *editorProxy) KeyboardMode() wicore.KeyboardMode { 125 | return e.keyboardMode 126 | } 127 | 128 | func (e *editorProxy) Version() string { 129 | return e.version 130 | } 131 | 132 | // Main is the function to call from your plugin to initiate the communication 133 | // channel between wi and your plugin. 134 | // 135 | // Returns the exit code that should be passed to os.Exit(). 136 | func Main(plugin wicore.Plugin) int { 137 | if os.ExpandEnv("${WI}") != "plugin" { 138 | fmt.Fprint(os.Stderr, "This is a wi plugin. This program is only meant to be run through wi itself.\n") 139 | return 1 140 | } 141 | // TODO(maruel): Take garbage from os.Stdin, put garbage in os.Stdout. 142 | fmt.Print(wicore.CalculateVersion()) 143 | 144 | // TODO(maruel): Pipe logs into os.Stderr and not have the editor process 145 | // kill the plugin process in this case. 146 | conn := wicore.MakeReadWriteCloser(os.Stdin, os.Stdout) 147 | server := rpc.NewServer() 148 | reg, rpc, deferred := makeEventRegistry() 149 | e := &editorProxy{ 150 | reg, 151 | deferred, 152 | "", 153 | nil, 154 | []string{}, 155 | wicore.Normal, 156 | "", 157 | } 158 | p := &pluginRPC{ 159 | e: e, 160 | conn: os.Stdin, 161 | plugin: plugin, 162 | } 163 | // Statically assert the interface is correctly implemented. 164 | var objPluginRPC internal.PluginRPC = p 165 | if err := server.RegisterName("PluginRPC", objPluginRPC); err != nil { 166 | fmt.Fprintf(os.Stderr, "%s\n", err) 167 | return 1 168 | } 169 | // Expose an object which doesn't have any method beside the ones exposed. 170 | // Otherwise it spew the logs with noise. 171 | objEventTriggerRPC := struct{ internal.EventTriggerRPC }{rpc} 172 | if err := server.RegisterName("EventTriggerRPC", objEventTriggerRPC); err != nil { 173 | fmt.Fprintf(os.Stderr, "%s\n", err) 174 | return 1 175 | } 176 | log.Printf("wicore.plugin.Main() now serving") 177 | server.ServeConn(conn) 178 | return 0 179 | } 180 | -------------------------------------------------------------------------------- /wicore/raster/raster.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Package raster implements text buffering. 6 | package raster 7 | 8 | import ( 9 | "fmt" 10 | "unicode/utf8" 11 | 12 | "github.com/wi-ed/wi/wicore/colors" 13 | ) 14 | 15 | // Rect is highly inspired by image.Rectangle but uses more standard origin + 16 | // size instead of two points. It makes the usage much simpler. It implements a 17 | // small subset of image.Rectangle. 18 | // 19 | // Negative values are invalid. 20 | type Rect struct { 21 | X, Y, Width, Height int 22 | } 23 | 24 | // Empty reports whether the rectangle area is 0. 25 | func (r Rect) Empty() bool { 26 | return r.Width == 0 || r.Height == 0 27 | } 28 | 29 | // In reports whether every point in r is in s. 30 | func (r Rect) In(s Rect) bool { 31 | if r.Empty() { 32 | return true 33 | } 34 | return s.X <= r.X && (r.X+r.Width) <= (s.X+s.Width) && s.Y <= r.Y && (r.Y+r.Height) <= (s.Y+s.Height) 35 | } 36 | 37 | // CellFormat describes all the properties of a single cell on screen. 38 | type CellFormat struct { 39 | Fg colors.RGB 40 | Bg colors.RGB 41 | Italic bool 42 | Underline bool 43 | Blinking bool 44 | } 45 | 46 | // Empty reports whether the CellFormat is black on black. In that case it 47 | // doesn't matter if it's italic or underlined. 48 | func (c CellFormat) Empty() bool { 49 | return c.Fg == colors.Black && c.Bg == colors.Black 50 | } 51 | 52 | // Cell represents the properties of a single character on screen. 53 | // 54 | // Some properties are ignored on different terminals. 55 | type Cell struct { 56 | R rune 57 | F CellFormat 58 | } 59 | 60 | // MakeCell is a shorthand to return a Cell. 61 | func MakeCell(R rune, Fg, Bg colors.RGB) Cell { 62 | return Cell{R, CellFormat{Fg: Fg, Bg: Bg}} 63 | } 64 | 65 | // CellStride is a slice of cells. 66 | type CellStride []Cell 67 | 68 | // Runes returns runes as a slice. 69 | func (c CellStride) Runes() []rune { 70 | out := make([]rune, len(c)) 71 | for i, cell := range c { 72 | out[i] = cell.R 73 | } 74 | return out 75 | } 76 | 77 | // Formats returns cells format as a slice. 78 | func (c CellStride) Formats() []CellFormat { 79 | out := make([]CellFormat, len(c)) 80 | for i, cell := range c { 81 | out[i] = cell.F 82 | } 83 | return out 84 | } 85 | 86 | // Buffer represents a buffer of Cells. 87 | // 88 | // The Cells slice can be shared across multiple Buffer when using SubBuffer(). 89 | // Width Height are guaranteed to be either both zero or non-zero. 90 | type Buffer struct { 91 | Width int 92 | Height int 93 | Stride int 94 | Cells CellStride 95 | } 96 | 97 | var emptySlice = CellStride{} 98 | 99 | func (b *Buffer) String() string { 100 | return fmt.Sprintf("Buffer(%d, %d, %d)", b.Width, b.Height, b.Stride) 101 | } 102 | 103 | // Line returns a single line in the buffer. 104 | // 105 | // If the requested line number if outside the buffer, an empty slice is 106 | // returned. 107 | func (b *Buffer) Line(Y int) CellStride { 108 | if Y >= b.Height { 109 | return emptySlice 110 | } 111 | base := Y * b.Stride 112 | return b.Cells[base : base+b.Width] 113 | } 114 | 115 | // Cell returns the pointer to a specific character cell. 116 | // 117 | // If the position is outside the buffer, an empty temporary cell is returned. 118 | func (b *Buffer) Cell(X, Y int) *Cell { 119 | line := b.Line(Y) 120 | if len(line) < X { 121 | return &Cell{} 122 | } 123 | return &line[X] 124 | } 125 | 126 | // DrawString draws a string into the buffer. 127 | // 128 | // Text will be automatically elided if necessary. 129 | func (b *Buffer) DrawString(s string, X, Y int, f CellFormat) { 130 | line := b.Line(Y) 131 | if len(line) <= X { 132 | return 133 | } 134 | bytes := []byte(ElideText(s, len(line)-X)) 135 | for x := X; x < len(line) && len(bytes) > 0; x++ { 136 | r, size := utf8.DecodeRune(bytes) 137 | line[x].R = r 138 | line[x].F = f 139 | bytes = bytes[size:] 140 | } 141 | } 142 | 143 | // Fill fills a buffer with a Cell. 144 | // 145 | // To fill a section of a buffer, use SubBuffer() first. 146 | func (b *Buffer) Fill(cell Cell) { 147 | if b.Height == 0 { 148 | return 149 | } 150 | // First set the initial line. 151 | line0 := b.Line(0) 152 | for x := 0; x < b.Width; x++ { 153 | line0[x] = cell 154 | } 155 | // Then used optimized copy() to fill the rest. 156 | for y := 1; y < b.Height; y++ { 157 | copy(b.Line(y), line0) 158 | } 159 | } 160 | 161 | // FormatText formats special characters like code points below 32. 162 | // 163 | // TODO(maruel): This must add coloring too. 164 | // 165 | // TODO(maruel): Improve performance for the common case (no special character). 166 | // 167 | // TODO(maruel): Handles special unicode whitespaces. Since the editor is meant 168 | // for mono-space font, all except U+0020 and \t should be escaped. 169 | // https://en.wikipedia.org/wiki/Whitespace_character 170 | func FormatText(s string) string { 171 | out := "" 172 | for _, c := range s { 173 | if c == 0 { 174 | out += "NUL" 175 | } else if c == 9 { 176 | // TODO(maruel): Need positional information AND desired tabwidth. 177 | out += string(c) 178 | } else if c <= 32 { 179 | out += "^" + string(c+'A'-1) 180 | } else { 181 | out += string(c) 182 | } 183 | } 184 | return out 185 | } 186 | 187 | // ElideText elide a string as necessary. 188 | func ElideText(s string, width int) string { 189 | if width <= 0 { 190 | return "" 191 | } 192 | length := utf8.RuneCountInString(s) 193 | if length <= width { 194 | return s 195 | } 196 | return s[:width-1] + "…" 197 | } 198 | 199 | // Blit copies src into b. 200 | // 201 | // To copy a section of a buffer, use SubBuffer() first. Areas that falls 202 | // either outside of src or of b are ignored. 203 | func (b *Buffer) Blit(src *Buffer) { 204 | for y := 0; y < src.Height; y++ { 205 | copy(b.Line(y), src.Line(y)) 206 | } 207 | } 208 | 209 | // SubBuffer returns a Buffer representing a section of the buffer, sharing the 210 | // same cells. 211 | func (b *Buffer) SubBuffer(r Rect) *Buffer { 212 | if r.X+r.Width > b.Width { 213 | r.Width = b.Width - r.X 214 | } 215 | if r.Y+r.Height > b.Height { 216 | r.Height = b.Height - r.Y 217 | } 218 | if r.Width <= 0 || r.Height <= 0 { 219 | return &Buffer{Cells: CellStride{}} 220 | } 221 | base := r.Y*b.Stride + r.X 222 | length := r.Height*b.Stride + r.Width - b.Width 223 | return &Buffer{ 224 | r.Width, 225 | r.Height, 226 | b.Stride, 227 | b.Cells[base : base+length], 228 | } 229 | } 230 | 231 | // NewBuffer creates a fresh new buffer. 232 | func NewBuffer(width, height int) *Buffer { 233 | if width <= 0 || height <= 0 { 234 | width = 0 235 | height = 0 236 | } 237 | return &Buffer{ 238 | width, 239 | height, 240 | width, 241 | make(CellStride, width*height), 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /wicore/raster/raster_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package raster 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/maruel/ut" 11 | "github.com/wi-ed/wi/wicore/colors" 12 | ) 13 | 14 | func TestRect(t *testing.T) { 15 | ut.AssertEqual(t, true, Rect{}.Empty()) 16 | ut.AssertEqual(t, true, Rect{}.In(Rect{})) 17 | ut.AssertEqual(t, true, Rect{1, 1, 2, 2}.In(Rect{0, 0, 10, 10})) 18 | } 19 | 20 | func TestCellFormat(t *testing.T) { 21 | ut.AssertEqual(t, true, CellFormat{}.Empty()) 22 | } 23 | 24 | func TestCell(t *testing.T) { 25 | ut.AssertEqual(t, Cell{R: 'a', F: CellFormat{Fg: colors.Red, Bg: colors.Blue}}, MakeCell('a', colors.Red, colors.Blue)) 26 | } 27 | 28 | func TestBuffer(t *testing.T) { 29 | ut.AssertEqual(t, "Buffer(0, 0, 0)", NewBuffer(-1, -1).String()) 30 | 31 | b := NewBuffer(4, 4) 32 | b2 := b.SubBuffer(Rect{1, 2, 1, 1}) 33 | b3 := b.SubBuffer(Rect{3, 3, 2, 2}) 34 | b.Fill(MakeCell('a', colors.Red, colors.Black)) 35 | b.DrawString("FOOO", 0, 0, CellFormat{Fg: colors.Red}) 36 | b.DrawString("foooo", 0, 3, CellFormat{colors.Brown, colors.Magenta, true, true, false}) 37 | // Outside. 38 | b.DrawString("ZZ", 4, 0, CellFormat{colors.Brown, colors.Magenta, true, true, false}) 39 | // Blit between two subbuffers inside the same buffer. 40 | b2.Blit(b3) 41 | b2.Cell(0, 0).F.Fg = colors.Blue 42 | 43 | ut.AssertEqual(t, Cell{}, *b.Cell(4, 4)) 44 | ut.AssertEqual(t, "FOOO", string(b.Line(0).Runes())) 45 | ut.AssertEqual(t, "aaaa", string(b.Line(1).Runes())) 46 | ut.AssertEqual(t, "a…aa", string(b.Line(2).Runes())) 47 | ut.AssertEqual(t, "foo…", string(b.Line(3).Runes())) 48 | 49 | fRed := CellFormat{Fg: colors.Red} 50 | fBlu := CellFormat{Fg: colors.Blue, Bg: colors.Magenta, Italic: true, Underline: true} 51 | fBro := CellFormat{Fg: colors.Brown, Bg: colors.Magenta, Italic: true, Underline: true} 52 | ut.AssertEqual(t, []CellFormat{fRed, fRed, fRed, fRed}, b.Line(0).Formats()) 53 | ut.AssertEqual(t, []CellFormat{fRed, fRed, fRed, fRed}, b.Line(1).Formats()) 54 | ut.AssertEqual(t, []CellFormat{fRed, fBlu, fRed, fRed}, b.Line(2).Formats()) 55 | ut.AssertEqual(t, []CellFormat{fBro, fBro, fBro, fBro}, b.Line(3).Formats()) 56 | 57 | ut.AssertEqual(t, Buffer{Cells: CellStride{}}, *b.SubBuffer(Rect{0, 0, -1, -1})) 58 | 59 | NewBuffer(0, 0).Fill(MakeCell('a', colors.Red, colors.Blue)) 60 | } 61 | 62 | func TestFormatText(t *testing.T) { 63 | data := [][]string{ 64 | {"hello", "hello"}, 65 | {"\000hello", "NULhello"}, 66 | {"\001", "^A"}, 67 | {" a", " a"}, 68 | } 69 | for i, v := range data { 70 | ut.AssertEqualIndex(t, i, v[1], FormatText(v[0])) 71 | } 72 | } 73 | 74 | func TestElideText(t *testing.T) { 75 | ut.AssertEqual(t, "", ElideText("foo", -1)) 76 | ut.AssertEqual(t, "", ElideText("foo", 0)) 77 | 78 | data := [][]string{ 79 | {"hel", "hel"}, 80 | {"hell", "he…"}, 81 | {"hello", "he…"}, 82 | {"\000hello", "\000h…"}, 83 | } 84 | for i, v := range data { 85 | ut.AssertEqualIndex(t, i, v[1], ElideText(v[0], 3)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /wicore/strings.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | package wicore 6 | 7 | import ( 8 | "github.com/wi-ed/wi/wicore/lang" 9 | ) 10 | 11 | // AliasFor describes that a command is an alias. 12 | var AliasFor = lang.Map{ 13 | lang.En: "Alias for \"%s\".", 14 | } 15 | 16 | // AliasNotFound describes an alias to another command did not resolve. 17 | var AliasNotFound = lang.Map{ 18 | lang.En: "\"%s\" is an alias to command \"%s\" but this command is not registered.", 19 | } 20 | -------------------------------------------------------------------------------- /wicore/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Marc-Antoine Ruel. All rights reserved. 2 | // Use of this source code is governed under the Apache License, Version 2.0 3 | // that can be found in the LICENSE file. 4 | 5 | // Utility functions. 6 | 7 | package wicore 8 | 9 | import ( 10 | "io" 11 | "reflect" 12 | 13 | "github.com/maruel/interfaceGUID" 14 | "github.com/wi-ed/wi/wicore/key" 15 | "github.com/wi-ed/wi/wicore/raster" 16 | ) 17 | 18 | // CalculateVersion returns the hex string of the hash of the primary 19 | // interfaces for this package. 20 | // 21 | // It traverses the Editor type recursively, expanding all types referenced 22 | // recursively. This data is used to generate an hash that represents the 23 | // "version" of this interface. 24 | func CalculateVersion() string { 25 | // TODO(maruel): EditorW, Plugin and PluginRPC. 26 | return interfaceGUID.CalculateGUID(reflect.TypeOf((*EditorW)(nil)).Elem()) 27 | } 28 | 29 | // GetKeyBindingCommand traverses the Editor's Window tree to find a View that 30 | // has the key binding in its Keyboard mapping. 31 | func GetKeyBindingCommand(e Editor, mode KeyboardMode, key key.Press) string { 32 | active := e.ActiveWindow() 33 | for { 34 | cmdName := active.View().KeyBindings().Get(mode, key) 35 | if cmdName != "" { 36 | return cmdName 37 | } 38 | active = active.Parent() 39 | if active == nil { 40 | return "" 41 | } 42 | } 43 | } 44 | 45 | // RootWindow returns the root Window when given any Window in the tree. 46 | func RootWindow(w Window) Window { 47 | for { 48 | if w.Parent() == nil { 49 | return w 50 | } 51 | w = w.Parent() 52 | } 53 | } 54 | 55 | // PositionOnScreen returns the exact position on screen of a Window. 56 | func PositionOnScreen(w Window) raster.Rect { 57 | out := w.Rect() 58 | if w.Docking() == DockingFloating { 59 | return out 60 | } 61 | for { 62 | w = w.Parent() 63 | if w == nil { 64 | break 65 | } 66 | // Take in account the parent Window position. 67 | r := w.Rect() 68 | out.X += r.X 69 | out.Y += r.Y 70 | if w.Docking() == DockingFloating { 71 | break 72 | } 73 | } 74 | return out 75 | } 76 | 77 | // MultiCloser closes multiple io.Closer at once. 78 | type MultiCloser []io.Closer 79 | 80 | // Close implements io.Closer. 81 | func (m MultiCloser) Close() (err error) { 82 | for _, i := range m { 83 | err1 := i.Close() 84 | if err1 != nil { 85 | err = err1 86 | } 87 | } 88 | return 89 | } 90 | 91 | // MakeReadWriteCloser creates a io.ReadWriteCloser out of one io.ReadCloser 92 | // and one io.WriteCloser. 93 | func MakeReadWriteCloser(reader io.ReadCloser, writer io.WriteCloser) io.ReadWriteCloser { 94 | return struct { 95 | io.Reader 96 | io.Writer 97 | io.Closer 98 | }{reader, writer, MultiCloser{reader, writer}} 99 | } 100 | --------------------------------------------------------------------------------