Why when opening a new file are the old files at the bottom?

When I open a new file with a browser-based editor I’m developing for myself as a learning exercise, I’ve hit the snag that the previous files remain at the bottom (with numbering restarting at 1).

What has me stumped is I’ve got a version which works fine using SWI Prolog communicating to the browser with JSON. Partly to learn Go, I’ve redone it using FormData, and have hit this weird snag of “ghosts of past files” remaining at the bottom of the CodeMirror object.

Firstly my HTML looks like this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Editor</title>
    <link rel="stylesheet" href="/css/codemirror.css">
    <link rel="stylesheet" href="/css/mystyle.css">
    <link rel="stylesheet" href="/theme/cobalt.css">
    <script src="/lib/codemirror.js"></script>
    <script src="/mode/css/css.js"></script>
    <script src="/mode/go/go.js"></script> 
    <script src="/mode/htmlmixed/htmlmixed.js"></script>
    <script src="/mode/http/http.js"></script>
    <script src="/mode/javascript/javascript.js"></script> 
    <script src="/mode/markdown/markdown.js"></script>
    <script src="/mode/shell/shell.js"></script>
    <script src="/mode/sql/sql.js"></script>
    <script src="/mode/toml/toml.js"></script>
    <script src="/mode/xml/xml.js"></script>
    <script src="/mode/yaml/yaml.js"></script>
  </head>
  <body>

    <form action="" method="post">
      <input type="text" id="filename" name="fn" size="40">
      <button type="button" id="load-button">Open...</button>
      <span id="filetime"></span>
      <button type="button" id="save-button">Save</button>
      <br>
      <textarea id="myTextArea"></textarea><br>
    </form>

    <p id="error"></p>

    <script src="/lib/editor.js"></script>
  </body>
</html>

Then my Javascript looks like this:

let myCodeMirror = CodeMirror.fromTextArea(document.querySelector("#myTextArea"),
					     { lineNumbers: true
                         , theme: "cobalt"
				         });

function getmode(filename) {
const modemap = { 'css': 'css'
                , 'el': 'erlang'
                , 'go': 'go'
                , 'html': 'htmlmixed'
                , 'js': 'javascript'
                , 'json': 'javascript'
                , 'md': 'markdown'
                , 'sh': 'shell'
                , 'sql': 'sql'
                , 'toml': 'toml'
                , 'yaml': 'yaml'
                };
  const dot_split = filename.split('.');
  const suffix = dot_split[dot_split.length - 1];
  return modemap[suffix];
}

function load_file(event) {
  event.preventDefault();
  const fd = new FormData();
  fd.append('cmd', 'load');
  fd.append('fn', document.querySelector('#filename').value);
  fetch('/form-handler', {
    method: 'POST',
    body: fd
  })
  .then(response => response.formData())
  .then(function(data) {
		myCodeMirror = CodeMirror.fromTextArea(document.querySelector("#myTextArea"),
					     { lineNumbers: true
					     , mode: getmode(fd.get('fn'))
                         , theme: "cobalt"
				         });
    if (data.has('error')) {
        document.querySelector("#error").textContent = data.get('error');
        } else {
        myCodeMirror.setValue(data.get('content'));
        document.querySelector("#filetime").textContent = data.get('modtime');
    }
  });
}

function save_file(event) {
  event.preventDefault();
  const fd = new FormData();
  fd.append('cmd', 'save');
  fd.append('fn', document.querySelector('#filename').value);
  fd.append('content', myCodeMirror.getValue())
  fetch('/form-handler', {
    method: 'POST',
    body: fd
  })
  .then(response => response.formData())
  .then(function(data) {
    if (data.has('error')) {
        document.querySelector("#error").textContent = data.get('error');
        } else {
        document.querySelector("#filetime").textContent = data.get('modtime');
    }
  });
}

function keyListener(event) {
  if (event.ctrlKey) {
    switch(event.key) {
      case 's':
        event.preventDefault();
        save_file(event);
        return true;
      default:
        return false;
    }
  } else {
    return false;
  }
}

document.querySelector('#load-button').addEventListener('click', load_file);
document.querySelector('#save-button').addEventListener('click', save_file);
document.addEventListener('keydown', keyListener);

And finally my go server code looks like this:

package main

import (
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"time"
)

func handler(resp http.ResponseWriter, req *http.Request) {
	username, password, ok := req.BasicAuth()
	if ok && username == "MyUserName" && password == "MyPassword" {
		req.ParseMultipartForm(2097152)
		mw := multipart.NewWriter(resp)
		resp.Header().Set("Content-Type", mw.FormDataContentType())
		resp.WriteHeader(http.StatusOK)
		switch req.FormValue("cmd") {
		case "load":
			content, err := os.ReadFile(req.FormValue("fn"))
			if err == nil {
				mw.WriteField("content", string(content))
				fp, _ := os.Open(req.FormValue("fn"))
				info, _ := fp.Stat()
				mw.WriteField("modtime", info.ModTime().Format(time.RFC3339))
				fp.Close()
			} else {
              mw.WriteField("error", "Loading problem")
            }
		case "save":
			err := os.WriteFile(req.FormValue("fn"), []byte(req.FormValue("content")), 0644)
			if err == nil {
				fp, _ := os.Open(req.FormValue("fn"))
				info, _ := fp.Stat()
				mw.WriteField("modtime", info.ModTime().Format(time.RFC3339))
				fp.Close()
            } else {
              mw.WriteField("error", "Saving problem")
            }
		default:
			mw.WriteField("error", "Unknown command")
		}
		mw.Close()
	} else {
		resp.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
		http.Error(resp, "Unauthorized", http.StatusUnauthorized)
	}
}

func main() {
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

The weird thing, I don’t have the same problem doing nearly the identical thing using Prolog with JSON. I’m struggling to understand FormData along with Go, so not sure if the problem is CodeMirror or where?

This is definitely not related to CodeMirror.

I found the bug in my JavaScript code.

I thought that the myCodeMirror = CodeMirror.fromTextArea(document.querySelector("#myTextArea").... in my load_file function would create a new instance, but instead it appended an additional CodeMirror child to the one already attached to the TextArea.

So the fix was to replace that with myCodeMirror.setOption("mode", getmode(fd.get('fn'))); and now it all works nicely.

The reason my previous JSON server worked and I encountered this problem with my rewrite to using “mime/multipart” had nothing to do with the messaging format, but that my previous version read the file name from the URL, so needed the entire page refreshed every time, thereby avoiding the “listing” of additional CodeMirror intances in a single TextArea.

So all very educational for me.

My next challenge is to write a Prolog mode for CodeMirror.