Ely Office: six offline-first desktop apps in Rust and GPUI

ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
// CodeMirror 6: the Markdown source.
import { basicSetup, EditorView } from 'codemirror'
import { Prec } from '@codemirror/state'
import { indentWithTab, redo, redoDepth, undoDepth } from '@codemirror/commands'
import { keymap } from '@codemirror/view'
import { markdown as language } from '@codemirror/lang-markdown'
import { languages } from '@codemirror/language-data'
import { ensureSyntaxTree } from '@codemirror/language'
import { theme } from './theme.js'
import { search } from '@codemirror/search'
import { textFinder } from './find.js'
/** Alt text that cannot close its brackets. */
const escape = (alt) => alt.replace(/[\\[\]]/g, '\\$&')
export function source(root, markdown, changed, edge) {
const view = new EditorView({
doc: markdown,
extensions: [
basicSetup,
language({ codeLanguages: languages }),
EditorView.lineWrapping,
theme,
search(),
// Tab indents; it never leaves the editor.
keymap.of([indentWithTab]),
// Past their history, undo and redo reach the page.
Prec.low(
keymap.of([
{ key: 'Mod-z', run: (view) => undoDepth(view.state) === 0 && edge('undo') },
{ key: 'Mod-Shift-z', run: (view) => redoDepth(view.state) === 0 && edge('redo') },
{ key: 'Mod-y', run: (view) => redoDepth(view.state) === 0 && edge('redo') },
]),
),
EditorView.updateListener.of((update) => {
if (update.docChanged) changed(() => update.state.doc.toString())
}),
],
parent: root,
})
// ATX and setext headings from the parse, with their starts.
const headings = () => {
const found = []
const tree = ensureSyntaxTree(view.state, view.state.doc.length, 200)
tree?.iterate({
enter: ({ name, from, to }) => {
const level = /^(?:ATX|Setext)Heading(\d)$/.exec(name)?.[1]
if (!level) return
const head = view.state.doc.sliceString(from, to).split('\n')[0]
const title = head.replace(/^#{1,6}[ \t]*/, '').replace(/[ \t]+#+[ \t]*$/, '')
found.push({ level: Number(level), title: title.trim(), from })
return false
},
})
return found
}
const reveal = (from) => {
view.dispatch({ selection: { anchor: from }, scrollIntoView: true })
view.focus()
}
return {
// Images at the caret, as Markdown.
insert: (images) => {
const text = images.map(({ src, alt }) => `![${escape(alt)}](${src})`).join('\n')
view.dispatch(view.state.replaceSelection(text))
view.focus()
return true
},
find: textFinder(view),
outline: () => headings().map(({ level, title }) => ({ level, title })),
heading: (key) => {
const found = headings()[key]
if (found) reveal(found.from)
return Boolean(found)
},
line: (n, query) => {
if (n < 1 || n > view.state.doc.lines) return false
const line = view.state.doc.line(n)
const column = line.text.toLowerCase().indexOf(query.toLowerCase())
reveal(line.from + Math.max(0, column))
return true
},
close: () => view.destroy(),
center: () => {
const { head } = view.state.selection.main
view.dispatch({ effects: EditorView.scrollIntoView(head, { y: 'center' }) })
},
where: () => {
const { head } = view.state.selection.main
const line = view.state.doc.lineAt(head)
return { line: line.number, column: head - line.from }
},
at: (n, column) => {
if (n < 1 || n > view.state.doc.lines) return false
const line = view.state.doc.line(n)
reveal(line.from + Math.min(column, line.length))
return true
},
text: () => view.state.doc.toString(),
// The text once every undone step is redone.
redone: () => {
let state = view.state
if (!redoDepth(state)) return null
while (redoDepth(state)) redo({ state, dispatch: (tr) => (state = tr.state) })
return state.doc.toString()
},
focus: () => view.focus(),
}
}