Add ability to change language using a language selector dialog

This commit is contained in:
Jonatan Heyman 2023-01-16 21:30:30 +01:00
parent 41573820ae
commit 8214bf1bbb
12 changed files with 334 additions and 78 deletions

View File

@ -1,12 +1,13 @@
<script>
import StatusBar from './StatusBar.vue'
import Editor from './Editor.vue'
import LanguageSelector from './LanguageSelector.vue'
export default {
components: {
Editor,
StatusBar,
LanguageSelector,
},
data() {
@ -19,6 +20,7 @@
initialTheme: window.darkMode.initial,
systemTheme: 'system',
development: window.location.href.indexOf("dev=1") !== -1,
showLanguageSelector: false,
}
},
@ -56,35 +58,65 @@
this.language = e.language
this.languageAuto = e.languageAuto
},
openLanguageSelector() {
this.showLanguageSelector = true
},
closeLanguageSelector() {
this.showLanguageSelector = false
this.$refs.editor.focus()
},
onLanguageSelect(language) {
this.showLanguageSelector = false
this.$refs.editor.setLanguage(language)
},
},
}
</script>
<template>
<Editor
@cursorChange="onCursorChange"
:theme="theme"
:development="development"
class="editor"
/>
<StatusBar
:line="line"
:column="column"
:language="language"
:languageAuto="languageAuto"
:theme="theme"
:systemTheme="systemTheme"
@toggleTheme="toggleTheme"
class="status"
/>
<div class="container">
<Editor
@cursorChange="onCursorChange"
:theme="theme"
:development="development"
class="editor"
ref="editor"
@openLanguageSelector="openLanguageSelector"
/>
<StatusBar
:line="line"
:column="column"
:language="language"
:languageAuto="languageAuto"
:theme="theme"
:systemTheme="systemTheme"
@toggleTheme="toggleTheme"
@openLanguageSelector="openLanguageSelector"
class="status"
/>
<div class="overlay">
<LanguageSelector
v-if="showLanguageSelector"
@select="onLanguageSelect"
@close="closeLanguageSelector"
/>
</div>
</div>
</template>
<style scoped lang="sass">
.editor
height: calc(100% - 21px)
.status
position: absolute
bottom: 0
left: 0
.container
width: 100%
height: 100%
position: relative
.editor
height: calc(100% - 21px)
.status
position: absolute
bottom: 0
left: 0
</style>

View File

@ -10,6 +10,7 @@
Welcome to Heynote!
[${modChar} + Enter] Insert new note block
[${modChar} + L] Change block language
[${modChar} + Down] Goto next block
[${modChar} + Up] Goto previous block
[${modChar} + A] Select all text in a note block. Press again to select the whole scratchpad
@ -32,6 +33,10 @@ Welcome to Heynote!
})
})
this.$refs.editor.addEventListener("openLanguageSelector", (e) => {
this.$emit("openLanguageSelector")
})
this.editor = new HeynoteEditor({
element: this.$refs.editor,
content: this.development ? testContent : initialContent,
@ -44,6 +49,21 @@ Welcome to Heynote!
this.editor.setTheme(newTheme)
},
},
methods: {
setLanguage(language) {
if (language === "auto") {
this.editor.setCurrentLanguage("text", true)
} else {
this.editor.setCurrentLanguage(language, false)
}
this.editor.focus()
},
focus() {
this.editor.focus()
},
},
}
</script>

View File

@ -0,0 +1,142 @@
<script>
import { LANGUAGES } from '../editor/languages.js'
const items = LANGUAGES.map(l => {
return {
"token": l.token,
"name": l.name
}
}).sort((a, b) => {
return a.name.localeCompare(b.name)
})
items.unshift({token: "auto", name:"Auto-detect"})
export default {
data() {
return {
selected: 0,
filter: "",
}
},
mounted() {
this.$refs.container.focus()
this.$refs.input.focus()
},
computed: {
filteredItems() {
return items.filter((lang) => {
return lang.name.toLowerCase().indexOf(this.filter.toLowerCase()) !== -1
})
},
},
methods: {
onKeydown(event) {
if (event.key === "ArrowDown") {
this.selected = Math.min(this.selected + 1, this.filteredItems.length - 1)
event.preventDefault()
} else if (event.key === "ArrowUp") {
this.selected = Math.max(this.selected - 1, 0)
event.preventDefault()
} else if (event.key === "Enter") {
this.$emit("select", this.filteredItems[this.selected].token)
event.preventDefault()
} else if (event.key === "Escape") {
this.$emit("close")
event.preventDefault()
}
},
onInput(event) {
// reset selection
this.selected = 0
},
onFocusOut(event) {
let container = this.$refs.container
if (container !== event.relatedTarget && !container.contains(event.relatedTarget)) {
this.$emit("close")
}
},
}
}
</script>
<template>
<form class="language-selector" tabindex="-1" @focusout="onFocusOut" ref="container">
<input
type="text"
ref="input"
@keydown="onKeydown"
@input="onInput"
v-model="filter"
/>
<ul class="items">
<li
v-for="item, idx in filteredItems"
:key="item.token"
:class="idx === selected ? 'selected' : ''"
@click="$emit('select', item.token)"
>
{{ item.name }}
</li>
</ul>
</form>
</template>
<style scoped lang="sass">
=dark-mode()
@media (prefers-color-scheme: dark)
@content
.language-selector
font-size: 13px
padding: 10px
//background: #48b57e
background: #efefef
position: absolute
top: 0
left: 50%
transform: translateX(-50%)
border-radius: 0 0 5px 5px
box-shadow: 0 0 10px rgba(0,0,0,0.3)
+dark-mode
background: #151516
input
background: #fff
padding: 4px 5px
border: 1px solid #ccc
box-sizing: border-box
border-radius: 2px
width: 400px
margin-bottom: 10px
&:focus
outline: none
border: 1px solid #fff
outline: 2px solid #48b57e
+dark-mode
background: #3b3b3b
border: 1px solid #5a5a5a
&:focus
border: 1px solid #3b3b3b
.items
> li
border-radius: 3px
padding: 5px 12px
cursor: pointer
&:hover
background: #e2e2e2
&.selected
background: #48b57e
color: #fff
+dark-mode
color: rgba(255,255,255, 0.53)
&:hover
background: #29292a
&.selected
background: #1b6540
color: rgba(255,255,255, 0.87)
</style>

View File

@ -35,7 +35,10 @@
Col <span class="num">{{ column }}</span>
</div>
<div class="spacer"></div>
<div class="status-block lang clickable">
<div
@click="$emit('openLanguageSelector')"
class="status-block lang clickable"
>
{{ languageName }}
<span v-if="languageAuto" class="auto">(auto)</span>
</div>
@ -61,6 +64,7 @@
padding-right: 0px
display: flex
flex-direction: row
align-items: center
user-select: none
+dark-mode

View File

@ -1,2 +1,3 @@
@import "reset"
@import "font"
@import "base"

View File

@ -3,7 +3,7 @@ html, body
padding: 0
background: #fff
color: #444
font-family: 'Gill Sans','Gill Sans MT',Calibri,'Trebuchet MS',sans-serif
font-family: "Open Sans"
height: 100%
font-size: 12px
overscroll-behavior-y: none

31
src/css/reset.sass Normal file
View File

@ -0,0 +1,31 @@
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video
margin: 0
padding: 0
border: 0
font-size: 100%
font: inherit
vertical-align: baseline
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section
display: block
body
line-height: 1
ol, ul
list-style: none
blockquote, q
quotes: none
blockquote:before, blockquote:after, q:before, q:after
content: ''
content: none
table
border-collapse: collapse
border-spacing: 0
input
font-size: 1em

View File

@ -276,7 +276,7 @@ const blockLineNumbers = lineNumbers({
}
})
const emitCursorChange = (element) => ViewPlugin.fromClass(
const emitCursorChange = (editor) => ViewPlugin.fromClass(
class {
update(update) {
// if the selection changed or the language changed (can happen without selection change),
@ -285,7 +285,7 @@ const emitCursorChange = (element) => ViewPlugin.fromClass(
if (update.selectionSet || langChange) {
const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head)
const block = getActiveNoteBlock(update.state)
element.dispatchEvent(new SelectionChangeEvent({
editor.element.dispatchEvent(new SelectionChangeEvent({
cursorLine,
language: block?.language.name,
languageAuto: block?.language.auto,
@ -295,7 +295,7 @@ const emitCursorChange = (element) => ViewPlugin.fromClass(
}
)
export const noteBlockExtension = (element) => {
export const noteBlockExtension = (editor) => {
return [
blockState,
noteBlockWidget(),
@ -304,6 +304,6 @@ export const noteBlockExtension = (element) => {
preventFirstBlockFromBeingDeleted,
preventSelectionBeforeFirstBlock,
blockLineNumbers,
emitCursorChange(element),
emitCursorChange(editor),
]
}

View File

@ -76,6 +76,11 @@ export function changeLanguageTo(state, dispatch, block, language, auto) {
}
}
export function changeCurrentBlockLanguage(state, dispatch, language, auto) {
const block = getActiveNoteBlock(state)
changeLanguageTo(state, dispatch, block, language, auto)
}
export function gotoPreviousBlock({state, dispatch}) {
const blocks = state.facet(blockState)
const newSelection = EditorSelection.create(state.selection.ranges.map(sel => {

View File

@ -8,18 +8,20 @@ import { heynoteBase } from "./theme/base.js"
import { customSetup } from "./setup.js"
import { heynoteLang } from "./lang-heynote/heynote.js"
import { noteBlockExtension } from "./block/block.js"
import { changeCurrentBlockLanguage } from "./block/commands.js"
import { heynoteKeymap } from "./keymap.js"
import { languageDetection } from "./language-detection/autodetect.js"
export class HeynoteEditor {
constructor({element, content, focus=true, theme="light"}) {
this.element = element
this.theme = new Compartment
this.state = EditorState.create({
const state = EditorState.create({
doc: content || "",
extensions: [
heynoteKeymap,
heynoteKeymap(this),
//minimalSetup,
customSetup,
@ -31,7 +33,7 @@ export class HeynoteEditor {
return {top: 80, bottom: 80}
}),
heynoteLang(),
noteBlockExtension(element),
noteBlockExtension(this),
languageDetection(() => this.view),
// set cursor blink rate to 1 second
@ -45,7 +47,7 @@ export class HeynoteEditor {
})
this.view = new EditorView({
state: this.state,
state: state,
parent: element,
})
@ -58,11 +60,23 @@ export class HeynoteEditor {
}
}
focus() {
this.view.focus()
}
setTheme(theme) {
this.view.dispatch({
effects: this.theme.reconfigure(theme === "dark" ? heynoteDark : heynoteLight),
})
}
openLanguageSelector() {
this.element.dispatchEvent(new Event("open-language-selector"))
}
setCurrentLanguage(lang, auto=false) {
changeCurrentBlockLanguage(this.view.state, this.view.dispatch, lang, auto)
}
}

View File

@ -2,41 +2,48 @@ import { keymap } from "@codemirror/view"
import { indentWithTab, insertTab, indentLess, indentMore } from "@codemirror/commands"
import { insertNewNote, moveLineUp, selectAll, gotoPreviousBlock, gotoNextBlock } from "./block/commands.js";
export const heynoteKeymap = keymap.of([
{
key: "Tab",
preventDefault: true,
//run: insertTab,
run: indentMore,
},
{
key: 'Shift-Tab',
preventDefault: true,
run: indentLess,
},
{
key: "Mod-Enter",
preventDefault: true,
run: insertNewNote,
},
{
key: "Mod-a",
preventDefault: true,
run: selectAll,
},
{
key: "Alt-ArrowUp",
preventDefault: true,
run: moveLineUp,
},
{
key: "Mod-ArrowUp",
preventDefault: true,
run: gotoPreviousBlock,
},
{
key: "Mod-ArrowDown",
preventDefault: true,
run: gotoNextBlock,
},
])
export function heynoteKeymap(editor) {
return keymap.of([
{
key: "Tab",
preventDefault: true,
//run: insertTab,
run: indentMore,
},
{
key: 'Shift-Tab',
preventDefault: true,
run: indentLess,
},
{
key: "Mod-Enter",
preventDefault: true,
run: insertNewNote,
},
{
key: "Mod-a",
preventDefault: true,
run: selectAll,
},
{
key: "Alt-ArrowUp",
preventDefault: true,
run: moveLineUp,
},
{
key: "Mod-ArrowUp",
preventDefault: true,
run: gotoPreviousBlock,
},
{
key: "Mod-ArrowDown",
preventDefault: true,
run: gotoNextBlock,
},
{
key: "Mod-l",
preventDefault: true,
run: () => editor.openLanguageSelector(),
}
])
}

View File

@ -11,7 +11,7 @@ const HIGHLIGHTJS_TO_TOKEN = Object.fromEntries(LANGUAGES.map(l => [l.highlightj
export function languageDetection(getView) {
const previousBlockContent = []
const previousBlockContent = {}
let idleCallbackId = null
const detectionWorker = new Worker('langdetect-worker.js?worker');
@ -50,11 +50,6 @@ export function languageDetection(getView) {
cancelIdleCallback(idleCallbackId)
idleCallbackId = null
}
if (update.transactions.every(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE))) {
// don't run language detection if the change was triggered by a language change
//console.log("ignoring check after language change")
return
}
idleCallbackId = requestIdleCallback(() => {
idleCallbackId = null
@ -69,7 +64,12 @@ export function languageDetection(getView) {
break
}
}
if (block === null || block.language.auto === false) {
if (block === null) {
return
} else if (block.language.auto === false) {
// if language is not auto, set it's previousBlockContent to null so that we'll trigger a language detection
// immediately if the user changes the language to auto
delete previousBlockContent[idx]
return
}