307 lines
7.1 KiB
JavaScript
307 lines
7.1 KiB
JavaScript
const constants = {
|
|
configLocation: '.config.ini',
|
|
defaultConfig: `\
|
|
defaultDocumentPath = "untitled.txt"
|
|
`
|
|
}
|
|
|
|
function main() {
|
|
Util.syncAnnotation = $.Annotation.define()
|
|
|
|
app.open()
|
|
}
|
|
|
|
function App() {
|
|
const self = this
|
|
|
|
this.open = () => {
|
|
let nav = document.createElement('nav')
|
|
this.path = new PathPrompt()
|
|
|
|
nav.appendChild(this.path.dom)
|
|
document.body.appendChild(nav)
|
|
|
|
this.editor = new $.EditorView({
|
|
state: this.createState({
|
|
doc: localStorage[this.path.value]
|
|
}),
|
|
parent: document.body,
|
|
dispatchTransactions: transactions => {
|
|
if(this.views.length > 1) {
|
|
for(let i = 1; i < this.views.length; i++) {
|
|
transactions.forEach(transaction => Util.syncDispatch(transaction, this.editor, this.views[i].editor))
|
|
}
|
|
} else {
|
|
this.editor.update(transactions)
|
|
}
|
|
}
|
|
})
|
|
this.views.push(this.editor)
|
|
|
|
this.editor.focus()
|
|
}
|
|
|
|
this.saveDocument = () => {
|
|
localStorage[this.path.value] = this.editor.state.doc.toString()
|
|
}
|
|
|
|
this.openDocument = () => {
|
|
this.editor.setState(this.createDocumentState())
|
|
}
|
|
|
|
this.createDocumentState = () => {
|
|
return this.createState({
|
|
doc: localStorage[this.path.value]
|
|
})
|
|
}
|
|
|
|
this.refreshState = () => {
|
|
let newState = this.createState({
|
|
doc: app.editor.state.doc.toString()
|
|
})
|
|
|
|
this.editor.setState(newState)
|
|
return newState
|
|
}
|
|
|
|
const openConfig = () => {
|
|
let config = {}
|
|
|
|
if(localStorage[constants.configLocation]) {
|
|
config = parseConfig(localStorage[constants.configLocation])
|
|
} else {
|
|
config = parseConfig(constants.defaultConfig)
|
|
localStorage[constants.configLocation] = constants.defaultConfig
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
const parseConfig = (text) => {
|
|
let data = {}
|
|
|
|
for(let line of text.split('\n')) {
|
|
if(line.length == 0 || line[0] == ';')
|
|
continue
|
|
|
|
let delimiterIndex = line.indexOf('=')
|
|
|
|
if(delimiterIndex == -1)
|
|
continue
|
|
|
|
let key = line.slice(0, delimiterIndex).trim()
|
|
let value = line.slice(delimiterIndex + 1).trim()
|
|
|
|
if(value[0] == '"') {
|
|
let quoteEnd = value.lastIndexOf('"')
|
|
|
|
value = value.slice(1, quoteEnd == 0 ? undefined : quoteEnd)
|
|
}
|
|
|
|
data[key] = value
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
this.createState = ({
|
|
doc,
|
|
extraKeys = [],
|
|
hasHistory = true
|
|
}) => {
|
|
let keymap = [
|
|
...$.closeBracketsKeymap,
|
|
...$.defaultKeymap,
|
|
...$.searchKeymap,
|
|
...$.foldKeymap,
|
|
...$.completionKeymap,
|
|
...$.lintKeymap,
|
|
$.indentWithTab,
|
|
{
|
|
key: 'Ctrl-s',
|
|
run(state, event) {
|
|
event.preventDefault()
|
|
self.saveDocument()
|
|
}
|
|
},
|
|
{
|
|
key: 'Ctrl-o',
|
|
run(state, event) {
|
|
let mainSelection = self.editor.state.selection.main
|
|
|
|
if(!mainSelection.empty)
|
|
self.path.set(self.editor.state.sliceDoc(mainSelection.from, mainSelection.to).trim() )
|
|
|
|
self.openDocument()
|
|
},
|
|
shift(state, event) {
|
|
event.preventDefault()
|
|
let mainSelection = self.editor.state.selection.main
|
|
|
|
if(!mainSelection.empty) {
|
|
let url = new URL(window.location)
|
|
|
|
url.hash = self.editor.state.sliceDoc(mainSelection.from, mainSelection.to).trim()
|
|
window.open(url.href)
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'Ctrl-r',
|
|
run(state, event) {
|
|
event.preventDefault()
|
|
let mainSelection = self.editor.state.selection.main
|
|
|
|
if(!mainSelection.empty)
|
|
self.path.dom.value = self.editor.state.sliceDoc(mainSelection.from, mainSelection.to)
|
|
self.path.dom.select()
|
|
}
|
|
},
|
|
{
|
|
key: 'Ctrl-l',
|
|
run(state, event) {
|
|
self.editor.setState(self.createState({
|
|
doc: Object.keys(localStorage).join('\n')
|
|
}))
|
|
}
|
|
},
|
|
{
|
|
key: 'Ctrl-b',
|
|
run(state, event) {
|
|
app.views.push(new ChildWindow())
|
|
}
|
|
},
|
|
...extraKeys
|
|
]
|
|
|
|
if(hasHistory) {
|
|
keymap = keymap.concat($.historyKeymap)
|
|
}
|
|
|
|
let extensions = [
|
|
$.lineNumbers(),
|
|
$.highlightActiveLineGutter(),
|
|
$.highlightSpecialChars(),
|
|
$.foldGutter(),
|
|
$.drawSelection(),
|
|
$.dropCursor(),
|
|
$.EditorState.allowMultipleSelections.of(true),
|
|
$.indentOnInput(),
|
|
$.syntaxHighlighting($.defaultHighlightStyle, { fallback: true }),
|
|
$.bracketMatching(),
|
|
$.closeBrackets(),
|
|
$.autocompletion(),
|
|
$.rectangularSelection(),
|
|
$.crosshairCursor(),
|
|
$.highlightActiveLine(),
|
|
$.highlightSelectionMatches(),
|
|
$.keymap.of(keymap)
|
|
]
|
|
|
|
if(hasHistory) {
|
|
extensions.push($.history())
|
|
}
|
|
|
|
let state = $.EditorState.create({
|
|
doc,
|
|
extensions
|
|
})
|
|
|
|
return state
|
|
}
|
|
|
|
this.config = openConfig()
|
|
this.views = []
|
|
}
|
|
|
|
function PathPrompt() {
|
|
this.set = value =>
|
|
document.title = window.location.hash = this.value = this.dom.value = value
|
|
|
|
this.dom = document.createElement('input')
|
|
|
|
this.dom.addEventListener('focusout', (event) => {
|
|
this.dom.value = this.value
|
|
})
|
|
|
|
this.dom.addEventListener('keydown', (event) => {
|
|
switch(event.key) {
|
|
case 'Enter':
|
|
event.preventDefault()
|
|
this.set(this.dom.value)
|
|
sessionStorage.lastDocument = this.value
|
|
// app.openDocument()
|
|
app.editor.focus()
|
|
break
|
|
|
|
case 'Escape':
|
|
event.preventDefault()
|
|
app.editor.focus()
|
|
break
|
|
}
|
|
})
|
|
|
|
if(window.location.hash !== '') {
|
|
this.set(decodeURI(window.location.hash.slice(1) ) )
|
|
} else {
|
|
this.set(sessionStorage.lastDocument ?? app.config.defaultDocumentPath)
|
|
}
|
|
}
|
|
|
|
function ChildWindow(index) {
|
|
let self = this
|
|
|
|
let state = app.createState({
|
|
doc: app.refreshState().doc,
|
|
hasHistory: false,
|
|
extraKeys: [
|
|
{
|
|
key: 'Mod-z',
|
|
run: () => $.undo(app.editor)
|
|
},
|
|
{
|
|
key: 'Mod-y',
|
|
mac: "Mod-Shift-z",
|
|
run: () => $.redo(app.editor)
|
|
}
|
|
]
|
|
})
|
|
|
|
this.index = app.views.length
|
|
this.window = window.open('about:blank', '_blank')
|
|
console.log(this)
|
|
this.window.document.head.appendChild(document.getElementById('stylesheet').cloneNode(true))
|
|
this.editor = new $.EditorView({
|
|
state,
|
|
parent: this.window.document.body,
|
|
dispatch: transaction => {
|
|
for(let i = 0; i < app.views.length; i++) {
|
|
if(i == this.index)
|
|
continue
|
|
|
|
Util.syncDispatch(transaction, this.editor, app.editor)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const Util = {}
|
|
|
|
{
|
|
Util.syncDispatch = (transaction, source, target) => {
|
|
source.update([ transaction ])
|
|
|
|
if(!transaction.changes.empty && !transaction.annotation(Util.syncAnnotation)) {
|
|
let annotations = [ Util.syncAnnotation.of(true) ]
|
|
let userEvent = transaction.annotation($.Transaction.userEvent)
|
|
|
|
if(userEvent)
|
|
annotations.push($.Transaction.userEvent.of(userEvent))
|
|
|
|
target.dispatch({ changes: transaction.changes, annotations })
|
|
}
|
|
}
|
|
}
|
|
|
|
window.app = new App()
|
|
window.addEventListener('load', main) |