Integrate Editor into Vue app

Add status bar that shows the current line number, column and block language
This commit is contained in:
Jonatan Heyman 2023-01-14 19:51:24 +01:00
parent 51e89ad55c
commit 6dee4f81f2
13 changed files with 415 additions and 70 deletions

View File

@ -14,11 +14,11 @@
<!--<div id="app"></div>-->
<!--<script type="module" src="/src/main.ts"></script>-->
<!--<div id="app"></div>
<script type="module" src="src/main.js"></script>-->
<div id="app"></div>
<script type="module" src="src/main.js"></script>
<div id="editor" class="editor"></div>
<script type="module" src="src/editor/index.js"></script>
<!--<div id="editor" class="editor"></div>
<script type="module" src="src/editor/index.js"></script>-->
</body>
</html>

184
package-lock.json generated
View File

@ -28,6 +28,7 @@
"codemirror": "^6.0.1",
"electron": "^22.0.0",
"electron-builder": "^23.6.0",
"sass": "^1.57.1",
"typescript": "^4.9.4",
"vite": "^4.0.3",
"vite-plugin-electron": "^0.11.1",
@ -1436,6 +1437,19 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/app-builder-bin": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz",
@ -1633,6 +1647,15 @@
}
]
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -1665,6 +1688,18 @@
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@ -1864,6 +1899,33 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@ -2662,6 +2724,18 @@
"node": ">=10"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@ -2799,6 +2873,18 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/global-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
@ -3050,6 +3136,12 @@
],
"optional": true
},
"node_modules/immutable": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.2.tgz",
"integrity": "sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==",
"dev": true
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -3066,6 +3158,18 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-builtin-module": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz",
@ -3105,6 +3209,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -3114,12 +3227,33 @@
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/isbinaryfile": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
@ -3421,6 +3555,15 @@
"dev": true,
"optional": true
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@ -3602,6 +3745,18 @@
"node": ">=12.0.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -3710,6 +3865,23 @@
"truncate-utf8-bytes": "^1.0.0"
}
},
"node_modules/sass": {
"version": "1.57.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
"integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@ -4018,6 +4190,18 @@
"tmp": "^0.2.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",

View File

@ -46,6 +46,7 @@
"codemirror": "^6.0.1",
"electron": "^22.0.0",
"electron-builder": "^23.6.0",
"sass": "^1.57.1",
"typescript": "^4.9.4",
"vite": "^4.0.3",
"vite-plugin-electron": "^0.11.1",

View File

@ -1,51 +1,58 @@
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
<script>
import HelloWorld from './components/HelloWorld.vue'
import StatusBar from './components/StatusBar.vue'
import Editor from './components/Editor.vue'
console.log("[App.vue]", `Hello world from Electron ${process.versions.electron}!`)
export default {
components: {
HelloWorld,
Editor,
StatusBar,
},
data() {
return {
line: 1,
column: 1,
language: "plaintext",
languageAuto: true,
}
},
methods: {
onCursorChange(e) {
//console.log("onCursorChange:", e)
this.line = e.cursorLine.line
this.column = e.cursorLine.col
this.language = e.language
this.languageAuto = e.languageAuto
},
},
}
console.log("[App.vue]", `Hello world from Electron ${process.versions.electron}!`)
</script>
<template>
<div>
<a href="https://www.electronjs.org/" target="_blank">
<img src="./assets/electron.svg" class="logo electron" alt="Electron logo" />
</a>
<a href="https://vitejs.dev/" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Electron + Vite + Vue" />
<div class="flex-center">
Place static files into the <code>/public</code> folder
<img style="width:5em;" src="/node.svg" alt="Node logo">
</div>
<Editor
@cursorChange="onCursorChange"
class="editor"
/>
<StatusBar
:line="line"
:column="column"
:language="language"
:languageAuto="languageAuto"
class="status"
/>
</template>
<style>
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo.electron:hover {
filter: drop-shadow(0 0 2em #9FEAF9);
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
<style scoped lang="sass">
.editor
height: calc(100% - 21px)
.status
position: absolute
bottom: 0
left: 0
</style>

35
src/components/Editor.vue Normal file
View File

@ -0,0 +1,35 @@
<script>
import { ref, shallowRef } from 'vue'
import { HeynoteEditor } from '../editor/editor.js'
import initialData from "../editor/fixture.js"
export default {
mounted() {
this.$refs.editor.addEventListener("selectionChange", (e) => {
//console.log("selectionChange:", e)
this.$emit("cursorChange", {
cursorLine: e.cursorLine,
language: e.language,
languageAuto: e.languageAuto,
})
})
const editor = new HeynoteEditor({
element: this.$refs.editor,
//content: "\ntext\n",
content: initialData,
})
},
}
</script>
<template>
<div class="editor" ref="editor"></div>
</template>
<style>
.editor {
width: 100%;
background-color: #f1f1f1;
}
</style>

View File

@ -0,0 +1,77 @@
<script>
const LANGUAGE_NAMES = {
"text": "Plain Text",
"javascript": "JavaScript",
"json": "JSON",
"python": "Python",
"html": "HTML",
"sql": "SQL",
"markdown": "Markdown",
"java": "Java",
"lezer": "Lezer",
"php": "PHP",
}
export default {
props: [
"line",
"column",
"language",
"languageAuto",
],
mounted() {
},
computed: {
languageName() {
return LANGUAGE_NAMES[this.language] || this.language
},
},
}
</script>
<template>
<div class="status">
<div class="status-block line-number">
Ln <span class="num">{{ line }}</span>
Col <span class="num">{{ column }}</span>
</div>
<div class="spacer"></div>
<div class="status-block lang">
{{ languageName }}
<span v-if="languageAuto" class="auto">(auto)</span>
</div>
</div>
</template>
<style scoped lang="sass">
.status
box-sizing: border-box
height: 22px
width: 100%
background-color: #48b57e
color: #fff
font-family: "Open Sans"
font-size: 12px
padding-left: 7px
padding-right: 7px
display: flex
flex-direction: row
.spacer
flex-grow: 1
.status-block
padding: 2px 5px
cursor: default
&.line-number
color: rgba(255, 255, 255, 0.7)
.num
color: rgba(255, 255, 255, 1.0)
&.lang
.auto
color: rgba(255, 255, 255, 0.7)
</style>

View File

@ -6,6 +6,7 @@ import { syntaxTree, ensureSyntaxTree } from "@codemirror/language"
import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js"
import { IterMode } from "@lezer/common";
import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js";
import { SelectionChangeEvent } from "../event.js"
// tracks the size of the first delimiter
@ -246,22 +247,49 @@ const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr) =
return tr
})
export function getBlockLineFromPos(state, pos) {
const line = state.doc.lineAt(pos)
const block = state.facet(blockState).find(block => block.content.from <= line.from && block.content.to >= line.from)
if (block) {
const firstBlockLine = state.doc.lineAt(block.content.from).number
return {
line: line.number - firstBlockLine + 1,
col: pos - line.from + 1,
length: line.length,
}
}
return null
}
const blockLineNumbers = lineNumbers({
formatNumber(lineNo, state) {
if (state.doc.lines >= lineNo) {
const lineOffset = state.doc.line(lineNo).from
const block = state.facet(blockState).find(block => block.content.from <= lineOffset && block.content.to >= lineOffset)
if (block) {
const firstBlockLine = state.doc.lineAt(block.content.from).number
return lineNo - firstBlockLine + 1
const lineInfo = getBlockLineFromPos(state, state.doc.line(lineNo).from)
if (lineInfo !== null) {
return lineInfo.line
}
}
return ""
}
})
export const noteBlockExtension = () => {
const emitCursorChange = (element) => ViewPlugin.fromClass(
class {
update(update) {
if (update.selectionSet) {
const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head)
const block = getActiveNoteBlock(update.state)
element.dispatchEvent(new SelectionChangeEvent({
cursorLine,
language: block?.language.name,
languageAuto: block?.language.auto,
}))
}
}
}
)
export const noteBlockExtension = (element) => {
return [
blockState,
noteBlockWidget(),
@ -270,5 +298,6 @@ export const noteBlockExtension = () => {
preventFirstBlockFromBeingDeleted,
preventSelectionBeforeFirstBlock,
blockLineNumbers,
emitCursorChange(element),
]
}

View File

@ -1,5 +1,5 @@
import { Annotation, EditorState, Compartment } from "@codemirror/state"
import { EditorView, keymap, drawSelection } from "@codemirror/view"
import { EditorView, keymap, drawSelection, ViewPlugin } from "@codemirror/view"
import { indentUnit, forceParsing } from "@codemirror/language"
import { heynoteLight } from "./theme/light.js"
@ -12,10 +12,6 @@ import { heynoteKeymap } from "./keymap.js"
import { languageDetection } from "./language-detection/autodetect.js"
// hide loading screen
postMessage({ payload: 'removeLoading' }, '*')
export class HeynoteEditor {
constructor({element, content, focus=true}) {
this.state = EditorState.create({
@ -35,7 +31,7 @@ export class HeynoteEditor {
return {top: 80, bottom: 80}
}),
heynoteLang(),
noteBlockExtension(),
noteBlockExtension(element),
languageDetection(() => this.view),
// set cursor blink rate to 1 second

8
src/editor/event.js Normal file
View File

@ -0,0 +1,8 @@
export class SelectionChangeEvent extends Event {
constructor({cursorLine, language, languageAuto}) {
super("selectionChange")
this.cursorLine = cursorLine
this.language = language
this.languageAuto = languageAuto
}
}

View File

@ -10,13 +10,19 @@ body {
overscroll-behavior-y: none;
}
#editor {
#app {
height: 100%;
width: 100%;
}
.editor {
height: 100%;
}
#editor .cm-editor {
.editor .cm-editor {
height: 100%;
}
#syntaxTree {
height: 20%;
overflow-y: auto;

View File

@ -2,6 +2,9 @@ import { EditorView } from '@codemirror/view';
export const heynoteBase = EditorView.theme({
"&.cm-editor.cm-focused": {
outline: "none",
},
".cm-content": {
paddingTop: 4,
},

View File

@ -2,8 +2,7 @@ import { EditorView } from "@codemirror/view";
export const heynoteLight = EditorView.theme({
"&": {
//color: base04,
backgroundColor: "#dfdfdf",
backgroundColor: "#fff",
},
".cm-cursor, .cm-dropCursor": {
borderLeftColor: "#000",

View File

@ -1,10 +1,10 @@
import { createApp } from 'vue'
import "./style.css"
import App from './App.vue'
import './samples/node-api'
createApp(App)
.mount('#app')
.$nextTick(() => {
const app = createApp(App)
app.mount('#app').$nextTick(() => {
// hide loading screen
postMessage({ payload: 'removeLoading' }, '*')
})
})