From 2aee52e06c5edd61533c0a95d70433daeab392ad Mon Sep 17 00:00:00 2001
From: Thomas Miceli <tho.miceli@gmail.com>
Date: Mon, 3 Apr 2023 00:40:39 +0200
Subject: [PATCH] Added indent type, indent size and wrap mode to editors

---
 internal/web/run.go             |   5 ++
 package-lock.json               |  16 ++++
 package.json                    |   3 +
 public/editor.js                | 145 ++++++++++++++++++++++++--------
 templates/base/gist_header.html |   2 +-
 templates/pages/create.html     |  23 ++++-
 templates/pages/edit.html       |  23 ++++-
 7 files changed, 179 insertions(+), 38 deletions(-)

diff --git a/internal/web/run.go b/internal/web/run.go
index f4aa715..9a83386 100644
--- a/internal/web/run.go
+++ b/internal/web/run.go
@@ -16,6 +16,7 @@ import (
 	"opengist/internal/config"
 	"opengist/internal/git"
 	"opengist/internal/models"
+	"os"
 	"path/filepath"
 	"regexp"
 	"strconv"
@@ -23,6 +24,7 @@ import (
 	"time"
 )
 
+var devAssets = os.Getenv("DEV_ASSETS") == "1"
 var store *sessions.CookieStore
 var re = regexp.MustCompile("[^a-z0-9]+")
 var fm = template.FuncMap{
@@ -75,6 +77,9 @@ var fm = template.FuncMap{
 		return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))))
 	},
 	"asset": func(jsfile string) string {
+		if devAssets {
+			return "http://localhost:16157/" + jsfile
+		}
 		return "/" + manifestEntries[jsfile].File
 	},
 }
diff --git a/package-lock.json b/package-lock.json
index b596989..659a6e7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,9 @@
       "devDependencies": {
         "@codemirror/commands": "^6.2.2",
         "@codemirror/lang-javascript": "^6.1.4",
+        "@codemirror/language": "^6.6.0",
+        "@codemirror/state": "^6.2.0",
+        "@codemirror/text": "^0.19.6",
         "@codemirror/view": "^6.9.3",
         "@tailwindcss/forms": "^0.5.3",
         "@tailwindcss/typography": "^0.5.9",
@@ -150,6 +153,13 @@
       "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==",
       "dev": true
     },
+    "node_modules/@codemirror/text": {
+      "version": "0.19.6",
+      "resolved": "https://registry.npmjs.org/@codemirror/text/-/text-0.19.6.tgz",
+      "integrity": "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA==",
+      "deprecated": "As of 0.20.0, this package has been merged into @codemirror/state",
+      "dev": true
+    },
     "node_modules/@codemirror/view": {
       "version": "6.9.3",
       "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.3.tgz",
@@ -4913,6 +4923,12 @@
       "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==",
       "dev": true
     },
+    "@codemirror/text": {
+      "version": "0.19.6",
+      "resolved": "https://registry.npmjs.org/@codemirror/text/-/text-0.19.6.tgz",
+      "integrity": "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA==",
+      "dev": true
+    },
     "@codemirror/view": {
       "version": "6.9.3",
       "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.3.tgz",
diff --git a/package.json b/package.json
index 092a1bd..3212651 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,9 @@
   "devDependencies": {
     "@codemirror/commands": "^6.2.2",
     "@codemirror/lang-javascript": "^6.1.4",
+    "@codemirror/language": "^6.6.0",
+    "@codemirror/state": "^6.2.0",
+    "@codemirror/text": "^0.19.6",
     "@codemirror/view": "^6.9.3",
     "@tailwindcss/forms": "^0.5.3",
     "@tailwindcss/typography": "^0.5.9",
diff --git a/public/editor.js b/public/editor.js
index c859ea9..cd57c5a 100644
--- a/public/editor.js
+++ b/public/editor.js
@@ -1,5 +1,6 @@
-import {EditorView, keymap, gutter, lineNumbers} from "@codemirror/view"
-import {indentWithTab} from "@codemirror/commands"
+import {EditorView, gutter, keymap, lineNumbers} from "@codemirror/view"
+import {Compartment, EditorState, Facet, SelectionRange} from "@codemirror/state"
+import {indentLess} from "@codemirror/commands";
 
 EditorView.theme({}, {dark: true})
 
@@ -8,24 +9,118 @@ let editorsParentdom = document.getElementById('editors')
 let allEditorsdom = document.querySelectorAll('#editors > .editor')
 let firstEditordom = allEditorsdom[0]
 
+const txtFacet = Facet.define({
+    combine(values) {
+        return values[0]
+    }
+})
+let indentSize = new Compartment, wrapMode = new Compartment, indentType = new Compartment
+
 const newEditor = (dom, value = '') => {
-    return new EditorView({
+    let editor = new EditorView({
         doc: value,
+        parent: dom,
         extensions: [
             lineNumbers(), gutter({class: "cm-mygutter"}),
-            keymap.of([indentWithTab]),
-        ],
-        parent: dom
+            keymap.of([{key: "Tab", run: customIndentMore, shift: indentLess}]),
+            indentSize.of(EditorState.tabSize.of(2)),
+            wrapMode.of([]),
+            indentType.of(txtFacet.of("space")),
+        ]
+    })
+
+    dom.querySelector('.editor-indent-type').onchange = (e) => {
+        let newTabType = e.target.value
+        setIndentType(editor, !['tab', 'space'].includes(newTabType) ? 'space' : newTabType)
+    }
+
+    dom.querySelector('.editor-indent-size').onchange = (e) => {
+        let newTabSize = parseInt(e.target.value)
+        setIndentSize(editor, ![2, 4, 8].includes(newTabSize) ? 2 : newTabSize)
+    }
+
+    dom.querySelector('.editor-wrap-mode').onchange = (e) => {
+        let newWrapMode = e.target.value
+        setLineWrapping(editor, newWrapMode === 'soft')
+    }
+
+    dom.addEventListener("drop", (e) => {
+        e.preventDefault(); // prevent the browser from opening the dropped file
+        e.target.closest('.editor').querySelector('input.form-filename').value = e.dataTransfer.files[0].name
+    });
+
+    // remove editor on delete
+    let deleteBtns = dom.querySelector('button.delete-file')
+    if (deleteBtns !== null) {
+        deleteBtns.onclick = () => {
+            editorsjs.splice(editorsjs.indexOf(editor), 1);
+            dom.remove()
+        }
+    }
+
+    editor.dom.addEventListener("input", function inputConfirmLeave() {
+        if (!editor.inView) return; // skip events outside the viewport
+
+        editor.dom.removeEventListener("input", inputConfirmLeave);
+        window.onbeforeunload = () => {
+            return 'Are you sure you want to quit?';
+        }
+    });
+
+    return editor;
+}
+
+function getIndentation(state) {
+    if (indentType.get(state).value === 'tab') {
+        return '\t';
+    }
+    return ' '.repeat(indentSize.get(state).value);
+}
+
+function customIndentMore({state, dispatch}) {
+    let indentation = getIndentation(state)
+    dispatch({
+        ...state.update(changeBySelectedLine(state, (line, changes) => {
+            changes.push({from: state.selection.ranges[0].from, insert: indentation})
+        })), selection: {
+            anchor: state.selection.ranges[0].from + indentation.length,
+            head: state.selection.ranges[0].from + indentation.length,
+        }
+    })
+    return true
+}
+
+function changeBySelectedLine(state, f) {
+    let atLine = -1
+    return state.changeByRange(range => {
+        let changes = []
+        for (let line = state.doc.lineAt(range.from); ;) {
+            if (line.number > atLine) {
+                f(line, changes)
+                atLine = line.number
+            }
+            if (range.to <= line.to) break
+            line = state.doc.lineAt(line.number + 1)
+        }
+        let changeSet = state.changes(changes)
+        return {changes, range: new SelectionRange(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1))}
     })
 }
 
-const eventOnDrop = (e) => {
-    e.preventDefault(); // prevent the browser from opening the dropped file
-    e.target.closest('.editor').querySelector('input.form-filename').value = e.dataTransfer.files[0].name
+function setIndentType(view, type) {
+    view.dispatch({effects: indentType.reconfigure(txtFacet.of(type))})
 }
 
-document.onsubmit = () => {
-    window.onbeforeunload = null;
+function setIndentSize(view, size) {
+    view.dispatch({effects: indentSize.reconfigure(EditorState.tabSize.of(size))})
+}
+
+function setLineWrapping(view, enable) {
+    if (enable) {
+        view.dispatch({effects: wrapMode.reconfigure(EditorView.lineWrapping)})
+    } else {
+        view.dispatch({effects: wrapMode.reconfigure([])})
+    }
 }
 
 let arr = [...allEditorsdom]
@@ -33,28 +128,6 @@ arr.forEach(el => {
     // in case we edit the gist contents
     let currEditor = newEditor(el, el.querySelector('.form-filecontent').value)
     editorsjs.push(currEditor)
-
-    currEditor.dom.addEventListener("input", function inputConfirmLeave()  {
-        if (!currEditor.inView) return; // skip events outside the viewport
-
-        currEditor.dom.removeEventListener("input", inputConfirmLeave);
-        window.onbeforeunload = () => {
-            return 'Are you sure you want to quit?';
-        }
-    });
-
-    currEditor.dom.addEventListener("drop", eventOnDrop);
-
-    // remove editor on delete
-    let deleteBtns = el.querySelector('button.delete-file')
-    if (deleteBtns !== null) {
-
-        deleteBtns.onclick = () => {
-
-            editorsjs.splice(editorsjs.indexOf(currEditor), 1);
-            el.remove()
-        }
-    }
 })
 
 document.getElementById('add-file').onclick = () => {
@@ -70,8 +143,6 @@ document.getElementById('add-file').onclick = () => {
     // creating the new codemirror editor and append it in the editor div
     editorsjs.push(newEditor(newEditorDom))
     editorsParentdom.append(newEditorDom)
-    editorsParentdom.addEventListener("drop", eventOnDrop);
-
 }
 
 document.querySelector('form#create').onsubmit = () => {
@@ -80,3 +151,7 @@ document.querySelector('form#create').onsubmit = () => {
         e.value = encodeURIComponent(editorsjs[j++].state.doc.toString())
     })
 }
+
+document.onsubmit = () => {
+    window.onbeforeunload = null;
+}
diff --git a/templates/base/gist_header.html b/templates/base/gist_header.html
index 5e1195e..ea06949 100644
--- a/templates/base/gist_header.html
+++ b/templates/base/gist_header.html
@@ -164,7 +164,7 @@
                             </div>
                         </div>
 
-                        <a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/archive/{{ .revision }}" class="whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-800 px-2.5 py-2 text-xs font-medium text-white shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
+                        <a href="/{{ .gist.User.Username }}/{{ .gist.Uuid }}/archive/{{ .revision }}" class="whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-800 px-2.5 py-2 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
                             Download ZIP</a>
                     </div>
                 </div>
diff --git a/templates/pages/create.html b/templates/pages/create.html
index eabde79..a75a476 100644
--- a/templates/pages/create.html
+++ b/templates/pages/create.html
@@ -24,10 +24,31 @@
             </div>
             <div id="editors" class="space-y-4">
                 <div class="rounded-md border border-1 border-gray-700 editor">
-                    <div class="border-b-1 border-gray-700 bg-gray-800 my-auto">
+                    <div class="border-b-1 border-gray-700 bg-gray-800 my-auto flex">
                         <p class="mx-2 my-2 inline-flex">
                             <input type="text" name="name" placeholder="Filename with extension" style="line-height: 0.05em" class="form-filename bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-700 rounded-md">
                         </p>
+                        <div class="mx-2 my-2 inline-flex ml-auto space-x-2">
+                            <select class="editor-indent-type whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
+                                <optgroup label="Indent mode">
+                                    <option value="space">Space</option>
+                                    <option value="tab">Tabs</option>
+                                </optgroup>
+                            </select>
+                            <select class="editor-indent-size whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
+                                <optgroup label="Indent size">
+                                    <option value="2">2</option>
+                                    <option value="4">4</option>
+                                    <option value="8">8</option>
+                                </optgroup>
+                            </select>
+                            <select class="editor-wrap-mode whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8  text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
+                                <optgroup label="Wrap mode">
+                                    <option value="no">No wrap</option>
+                                    <option value="soft">Soft wrap</option>
+                                </optgroup>
+                            </select>
+                        </div>
                     </div>
                     <input type="hidden" value="" name="content" class="form-filecontent">
                 </div>
diff --git a/templates/pages/edit.html b/templates/pages/edit.html
index 056a206..fe74dd9 100644
--- a/templates/pages/edit.html
+++ b/templates/pages/edit.html
@@ -55,7 +55,7 @@
             <div id="editors" class="space-y-4">
                 {{ range $file := .files }}
                 <div class="rounded-md border border-1 border-gray-700 editor">
-                    <div class="border-b-1 border-gray-700 bg-gray-800 my-auto">
+                    <div class="border-b-1 border-gray-700 bg-gray-800 my-auto flex">
                         <p class="mx-2 my-2 inline-flex">
                             <input type="text" value="{{ $file.Filename }}" name="name" placeholder="Filename with extension" style="line-height: 0.05em; z-index: 99999" class="form-filename bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-700 rounded-l-md">
                             <button style="line-height: 0.05em" class="delete-file -ml-px relative inline-flex items-center space-x-2 px-4 py-2 border border-gray-700 text-sm font-medium rounded-r-md text-slate-300 bg-gray-800 hover:bg-gray-900 focus:outline-none" type="button">
@@ -64,6 +64,27 @@
                                 </svg>
                             </button>
                         </p>
+                        <div class="mx-2 my-2 inline-flex ml-auto space-x-2">
+                            <select class="editor-indent-type whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
+                                <optgroup label="Indent mode">
+                                    <option value="space">Space</option>
+                                    <option value="tab">Tabs</option>
+                                </optgroup>
+                            </select>
+                            <select class="editor-indent-size whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8 text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
+                                <optgroup label="Indent size">
+                                    <option value="2">2</option>
+                                    <option value="4">4</option>
+                                    <option value="8">8</option>
+                                </optgroup>
+                            </select>
+                            <select class="editor-wrap-mode whitespace-nowrap text-slate-300 rounded border border-gray-600 bg-gray-900 pr-8  text-xs font-medium shadow-sm hover:bg-gray-700 hover:border-gray-500 hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500">
+                                <optgroup label="Wrap mode">
+                                    <option value="no">No wrap</option>
+                                    <option value="soft">Soft wrap</option>
+                                </optgroup>
+                            </select>
+                        </div>
                     </div>
                     <input type="hidden" value="{{ $file.Content }}" name="content" class="form-filecontent">
                 </div>