|
1 | 1 | # Adding a Text Editor
|
2 | 2 | Adding a text editor to make it possible to edit files in the filesystem.
|
3 | 3 |
|
4 |
| -The text editor for the fileystem makes use of the Chrome browser and the |
| 4 | +The text editor for the fileystem makes use of the Chrome browser and the Ace Javascript library that gives access to editor-like functionality.<br> |
| 5 | +One of the hardest challenges to this step was making an editor work for the Windows platform. The original idea was to make use of the <a href="https://github.com/bediger4000/kilo-in-go">Kilo-in-go</a> Golang terminal text editor to add a Vim-like editor to the filesystem. Though this was unsuccessful because of UNIX-like system calls the editor used and my inability to find equivalent Windows syscalls.<br><br> |
| 6 | +So this editor would run from the browser.<br><br> |
| 7 | +<img src="https://github.com/AlysonBee/GoVirtualFilesystem/blob/master/assets/webeditor.png" height="400" width="500" /> |
| 8 | + |
| 9 | + |
| 10 | +## Putting together a "website" text editor |
| 11 | + |
| 12 | +The first step was to write a web app in Go that could be turned on and off from the editor with the command "open". This I found to be a really Go specific thing because the original plan was to have a Python Flask app that could run and then be killed with signals but this didn't work at all.<br><br> |
| 13 | + |
| 14 | +### Opening and Closing the App |
| 15 | + |
| 16 | +This detail is reasonably important as it's the most creative part of this web app approach. Opening and closing the browser required a bit of out-of-the-box thinking. The main goal was to keep it as user-friendly and immersive as possible. I aimed to implement the functionality of Ctrl+S to save content and closing the browser tab as a way of closing the editor while also disabling the ability to crash the browser from the terminal. These three implementations went as follows; |
| 17 | + |
| 18 | +#### Opening a File |
| 19 | +``` |
| 20 | +open [filename] |
| 21 | +``` |
| 22 | +A small segment from `shell.go` that's responsible for opening a new editor session; |
| 23 | +``` |
| 24 | + .... |
| 25 | + if _, exists := tmp.files[segments[len(segments)-1]]; exists { |
| 26 | + editingFile = tmp.files[segments[len(segments)-1]] |
| 27 | + editor() |
| 28 | + //s.readFile(tmp.files[segments[len(segments)-1]].rootPath) |
| 29 | +
|
| 30 | + } |
| 31 | + ... |
| 32 | +``` |
| 33 | +The function `editor()` is responsible for spinning up a new web session with the contents of the global variable `editingFile` which would dump the target file's contents into the browser by dereferencing the `content` section of the object (`editingFile.content`').<br> |
| 34 | +<br><br> |
| 35 | + |
| 36 | +The first few lines of code after initializing a new server is to disable the `Ctrl+C` functionality to make crashing it from the terminla throw out an error message telling the user to close the browser tab. |
| 37 | +``` |
| 38 | + c := make(chan os.Signal, 1) |
| 39 | + signal.Notify(c, os.Interrupt) |
| 40 | + go func() { |
| 41 | + for sig := range c { |
| 42 | + fmt.Println("Interrupt cancelled. Close text editor tab at :127.0.0.1:5000;", sig) |
| 43 | + } |
| 44 | + }() |
| 45 | +``` |
| 46 | +The next piece of code opens the browser at the address `127.0.0.1:5000` and starts up our server to listen in on the same address. |
| 47 | +``` |
| 48 | + ... |
| 49 | + openbrowser("http://127.0.0.1:5000") |
| 50 | + server := http.Server{Addr: ":5000", Handler: mux} |
| 51 | + ... |
| 52 | +``` |
| 53 | +Finally, our endpoint is mapped to the function `indexHandler`. |
| 54 | +``` |
| 55 | + mux.HandleFunc("/", indexHandler) |
| 56 | +``` |
| 57 | +A global variable that sets up our template will allow us to send our `editingFile.content` to the HTML file `editor/editor.html`. The key issue with this design is the hardcoded path to `editor/editor.html`. With this path set this way, invoking the `open` command from anywhere that isn't the root of the directory will cause a crash. <b>This must be converted to an absolute path</b>. |
| 58 | + ``` |
| 59 | + ... |
| 60 | + var tpl = template.Must(template.ParseFiles("editor/editor.html")) |
| 61 | + ... |
| 62 | + ``` |
| 63 | + And lastly, our indexHandler code. |
| 64 | + ``` |
| 65 | + source := &sourceCode{ |
| 66 | + Code: string(editingFile.content), |
| 67 | + Ext: "golang", |
| 68 | + } |
| 69 | + err := tpl.Execute(buf, source) |
| 70 | +``` |
| 71 | +The Ace Editor library code responsible for displaying our source code in the editor inside `editor/editor.html` |
| 72 | +``` |
| 73 | +... |
| 74 | + var editor = ace.edit("editor"); |
| 75 | + editor.setTheme("ace/theme/monokai"); |
| 76 | + editor.session.setMode("ace/mode/{{ .Ext }}"); |
| 77 | + ... |
| 78 | + |
| 79 | + <div id="editor">{{ .Code }}</div> |
| 80 | + ... |
| 81 | +... |
| 82 | +``` |
| 83 | +#### Closing |
| 84 | + |
| 85 | +Closing our text editor made use of Javascript Window interaction code. This made use of an AJAX trigger on the `beforeunload` window event (which basically means "before the window closes") which sent a call to the `/shutdown` endpoint in our Go code.<br><br> |
| 86 | + |
| 87 | +Inside `editor/editor.html` |
| 88 | + |
| 89 | +``` |
| 90 | + $(window).on("beforeunload", function() { |
| 91 | + $.ajax({ |
| 92 | + type: 'POST', |
| 93 | + url: '/shutdown', |
| 94 | + contentType: 'application/json;charset=UTF-8', |
| 95 | + data: JSON.stringify({'data':'exiting'}) |
| 96 | + }) |
| 97 | + }) |
| 98 | +``` |
| 99 | +Inside `editor.go` |
| 100 | +``` |
| 101 | + mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) { |
| 102 | + if r.Method == "POST" { |
| 103 | + server.Shutdown(context.Background()) |
| 104 | + } |
| 105 | + }) |
| 106 | +``` |
| 107 | +The above code shuts down the running browser session and returns control back to the filesystem. |
| 108 | + |
| 109 | +#### Saving |
| 110 | +Saving made use of Javascript code for key mapping code. When a keypress is detected and the simutaneous keys for `Ctrl` and `S` are pressed, the `save` endpoint is hit with a POST request to the backend.<br><br> |
| 111 | + |
| 112 | +Inside `editor/editor.html` |
| 113 | +``` |
| 114 | + document.addEventListener("keydown", function(e) { |
| 115 | + if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83) { |
| 116 | + e.preventDefault(); |
| 117 | + console.log(editor.getValue()) |
| 118 | + $.ajax({ |
| 119 | + type: 'POST', |
| 120 | + url: '/save', |
| 121 | + contentType: 'application/json;charset=UTF-8', |
| 122 | + data: JSON.stringify({'data': editor.getValue()}), |
| 123 | + success: function() { |
| 124 | + console.log("success") |
| 125 | + } |
| 126 | + }); |
| 127 | + ``` |
| 128 | +Inside `editor.go` |
| 129 | + ``` |
| 130 | + func saveHandler(w http.ResponseWriter, r *http.Request) { |
| 131 | + if r.Method == "POST" { |
| 132 | + var data map[string]string |
| 133 | + json.NewDecoder(r.Body).Decode(&data) |
| 134 | + editingFile.content = []byte(data["data"https://itnext.io/go-virtual-filesystem-adding-a-text-editor-176f082e0109]) |
| 135 | + .} |
| 136 | +} |
| 137 | +``` |
| 138 | +The new "edited" code from the text editor overwrites the old value in `editingFile.content`.<br><br> |
| 139 | + |
| 140 | +A more detailed version of this implementation can be seen in the corresponding article <a href="https://itnext.io/go-virtual-filesystem-adding-a-text-editor-176f082e0109">here</a>. |
| 141 | + |
| 142 | + |
| 143 | + |
| 144 | + |
| 145 | + |
| 146 | + |
| 147 | + |
| 148 | + |
| 149 | + |
| 150 | + |
| 151 | + |
| 152 | + |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | + |
| 157 | + |
| 158 | + |
| 159 | + |
| 160 | + |
| 161 | + |
| 162 | + |
0 commit comments