feat: markdown editor with monaco (wip)

pull/6775/head
NGPixel 2 years ago
parent 97bcc56c65
commit 9e875794cd
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -68,6 +68,7 @@
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"mitt": "3.0.0",
"monaco-editor": "0.37.1",
"pako": "2.1.0",
"pinia": "2.0.33",
"prosemirror-commands": "1.5.1",
@ -5579,6 +5580,11 @@
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz",
"integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ=="
},
"node_modules/monaco-editor": {
"version": "0.37.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.37.1.tgz",
"integrity": "sha512-jLXEEYSbqMkT/FuJLBZAVWGuhIb4JNwHE9kPTorAVmsdZ4UzHAfgWxLsVtD7pLRFaOwYPhNG9nUCpmFL1t/dIg=="
},
"node_modules/ms": {
"version": "2.1.3",
"dev": true,

@ -73,6 +73,7 @@
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"mitt": "3.0.0",
"monaco-editor": "0.37.1",
"pako": "2.1.0",
"pinia": "2.0.33",
"prosemirror-commands": "1.5.1",

@ -39,7 +39,11 @@ module.exports = configure(function (/* ctx */) {
'apollo',
'components',
'eventbus',
'i18n'
'i18n',
{
server: false,
path: 'monaco'
}
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
@ -99,6 +103,15 @@ module.exports = configure(function (/* ctx */) {
'prosemirror-model',
'prosemirror-view'
]
viteConf.build.rollupOptions = {
external: ['monaco-editor'],
output: {
globals: {
'monaco-editor': 'monaco-editor'
}
}
}
},
// viteVuePluginOptions: {},

@ -0,0 +1,23 @@
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
self.MonacoEnvironment = {
getWorker (_, label) {
if (label === 'json') {
return new JsonWorker()
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new CssWorker()
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return new HtmlWorker()
}
if (label === 'typescript' || label === 'javascript') {
return new TsWorker()
}
return new EditorWorker()
}
}

@ -201,10 +201,10 @@
)
q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.togglePreviewPane') }}
//--------------------------------------------------------
//- CODEMIRROR
//- MONACO EDITOR
//--------------------------------------------------------
.editor-markdown-editor
textarea(ref='cmRef')
div(ref='monacoRef')
transition(name='editor-markdown-preview')
.editor-markdown-preview(v-if='state.previewShown')
.editor-markdown-preview-toolbar
@ -238,30 +238,14 @@ import { useMeta, useQuasar, setCssVar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { get, flatten, last, times, startsWith, debounce } from 'lodash-es'
import { DateTime } from 'luxon'
import * as monaco from 'monaco-editor'
import { Position, Range } from 'monaco-editor'
import { WorkspaceEdit } from '../helpers/monacoTypes'
import { useEditorStore } from 'src/stores/editor'
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
// Code Mirror
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
import '../css/codemirror.scss'
// Language
import 'codemirror/mode/markdown/markdown.js'
// Addons
import 'codemirror/addon/selection/active-line.js'
import 'codemirror/addon/display/fullscreen.js'
import 'codemirror/addon/display/fullscreen.css'
import 'codemirror/addon/selection/mark-selection.js'
import 'codemirror/addon/search/searchcursor.js'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css'
// Markdown Renderer
import { MarkdownRenderer } from 'src/renderers/markdown'
@ -281,8 +265,9 @@ const { t } = useI18n()
// STATE
let editor
const cm = shallowRef(null)
const cmRef = ref(null)
const monacoRef = ref(null)
const state = reactive({
previewShown: true,
@ -326,8 +311,8 @@ function insertTable () {
}
/**
* Set current line as header
*/
* Set current line as header
*/
function setHeaderLine (lvl) {
const curLine = cm.value.doc.getCursor('head').line
let lineContent = cm.value.doc.getLine(curLine)
@ -340,8 +325,8 @@ function setHeaderLine (lvl) {
}
/**
* Get the header lever of the current line
*/
* Get the header lever of the current line
*/
function getHeaderLevel (cm) {
const curLine = cm.doc.getCursor('head').line
const lineContent = cm.doc.getLine(curLine)
@ -354,16 +339,16 @@ function getHeaderLevel (cm) {
}
/**
* Insert content at cursor
*/
* Insert content at cursor
*/
function insertAtCursor ({ content }) {
const cursor = cm.value.doc.getCursor('head')
cm.value.doc.replaceRange(content, cursor)
}
/**
* Insert content after current line
*/
* Insert content after current line
*/
function insertAfter ({ content, newLine }) {
const curLine = cm.value.doc.getCursor('to').line
const lineLength = cm.value.doc.getLine(curLine).length
@ -371,8 +356,8 @@ function insertAfter ({ content, newLine }) {
}
/**
* Insert content before current line
*/
* Insert content before current line
*/
function insertBeforeEachLine ({ content, after }) {
let lines = []
if (!cm.value.doc.somethingSelected()) {
@ -399,27 +384,40 @@ function insertBeforeEachLine ({ content, after }) {
}
/**
* Insert an Horizontal Bar
*/
* Insert an Horizontal Bar
*/
function insertHorizontalBar () {
insertAfter({ content: '---', newLine: true })
}
/**
* Toggle Markup at selection
*/
function toggleMarkup ({ start, end }) {
* Toggle Markup at selection
*/
async function toggleMarkup ({ start, end }) {
if (!end) { end = start }
if (!cm.value.doc.somethingSelected()) {
if (!editor.getSelection()) {
return $q.notify({
type: 'negative',
message: t('editor.markup.noSelectionError')
})
}
cm.value.doc.replaceSelections(cm.value.doc.getSelections().map(s => start + s + end))
}
const onCmInput = debounce(processContent, 500)
const edits = []
for (const selection of editor.getSelections()) {
const selectedText = editor.getModel().getValueInRange(selection)
if (!selectedText) {
const word = editor.getModel().getWordAtPosition(selection.getPosition())
}
if (selectedText.startsWith(start) && selectedText.endsWith(end)) {
edits.push({ range: selection, text: selectedText.substring(start.length, selectedText.length - end.length) })
} else {
edits.push({ range: selection, text: `${start}${selectedText}${end}` })
}
}
editor.executeEdits('', edits)
}
function processContent (newContent) {
pageStore.$patch({
@ -435,75 +433,137 @@ onMounted(async () => {
hideSideNav: true
})
// -> Initialize CodeMirror
cm.value = CodeMirror.fromTextArea(cmRef.value, {
tabSize: 2,
mode: 'text/markdown',
theme: 'wikijs-dark',
lineNumbers: true,
lineWrapping: true,
line: true,
styleActiveLine: true,
highlightSelectionMatches: {
annotateScrollbar: true
},
viewportMargin: 50,
inputStyle: 'contenteditable',
allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'],
// direction: siteConfig.rtl ? 'rtl' : 'ltr',
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
// -> Define Monaco Theme
monaco.editor.defineTheme('wikijs', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#070a0d',
'editor.lineHighlightBackground': '#0d1117',
'editorLineNumber.foreground': '#546e7a',
'editorGutter.background': '#1e232a'
}
})
// -> Initialize Monaco Editor
editor = monaco.editor.create(monacoRef.value, {
value: pageStore.content,
language: 'markdown',
theme: 'wikijs',
automaticLayout: true,
scrollBeyondLastLine: false,
fontSize: 16,
formatOnType: true,
lineNumbersMinChars: 3
})
window.edd = editor
// -> Define Formatting Actions
editor.addAction({
contextMenuGroupId: 'markdown.extension.editing',
contextMenuOrder: 0,
id: 'markdown.extension.editing.toggleBold',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyB],
label: 'Toggle bold',
precondition: '',
run (ed) {
toggleMarkup({ start: '**' })
}
})
cm.value.setValue(pageStore.content)
cm.value.on('change', c => {
editor.addAction({
contextMenuGroupId: 'markdown.extension.editing',
contextMenuOrder: 0,
id: 'markdown.extension.editing.toggleItalic',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyI],
label: 'Toggle italic',
precondition: '',
run (ed) {
toggleMarkup({ start: '*' })
}
})
editor.onDidChangeModelContent(debounce(ev => {
editorStore.$patch({
lastChangeTimestamp: DateTime.utc()
})
pageStore.$patch({
content: c.getValue()
content: editor.getValue()
})
onCmInput(pageStore.content)
})
processContent(pageStore.content)
}, 500))
// -> Initialize CodeMirror
// cm.value = CodeMirror.fromTextArea(cmRef.value, {
// tabSize: 2,
// mode: 'text/markdown',
// theme: 'wikijs-dark',
// lineNumbers: true,
// lineWrapping: true,
// line: true,
// styleActiveLine: true,
// highlightSelectionMatches: {
// annotateScrollbar: true
// },
// viewportMargin: 50,
// inputStyle: 'contenteditable',
// allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'],
// // direction: siteConfig.rtl ? 'rtl' : 'ltr',
// foldGutter: true,
// gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
// })
cm.value.setSize(null, '100%')
// cm.value.setValue(pageStore.content)
// cm.value.on('change', c => {
// editorStore.$patch({
// lastChangeTimestamp: DateTime.utc()
// })
// pageStore.$patch({
// content: c.getValue()
// })
// onCmInput(pageStore.content)
// })
// cm.value.setSize(null, '100%')
// -> Set Keybindings
const keyBindings = {
'F11' (c) {
c.setOption('fullScreen', !c.getOption('fullScreen'))
},
'Esc' (c) {
if (c.getOption('fullScreen')) {
c.setOption('fullScreen', false)
}
},
[`${CtrlKey}-S`] (c) {
// save()
return false
},
[`${CtrlKey}-B`] (c) {
toggleMarkup({ start: '**' })
return false
},
[`${CtrlKey}-I`] (c) {
toggleMarkup({ start: '*' })
return false
},
[`${CtrlKey}-Alt-Right`] (c) {
let lvl = getHeaderLevel(c)
if (lvl >= 6) { lvl = 5 }
setHeaderLine(lvl + 1)
return false
},
[`${CtrlKey}-Alt-Left`] (c) {
let lvl = getHeaderLevel(c)
if (lvl <= 1) { lvl = 2 }
setHeaderLine(lvl - 1)
return false
}
}
cm.value.setOption('extraKeys', keyBindings)
// const keyBindings = {
// 'F11' (c) {
// c.setOption('fullScreen', !c.getOption('fullScreen'))
// },
// 'Esc' (c) {
// if (c.getOption('fullScreen')) {
// c.setOption('fullScreen', false)
// }
// },
// [`${CtrlKey}-S`] (c) {
// // save()
// return false
// },
// [`${CtrlKey}-B`] (c) {
// toggleMarkup({ start: '**' })
// return false
// },
// [`${CtrlKey}-I`] (c) {
// toggleMarkup({ start: '*' })
// return false
// },
// [`${CtrlKey}-Alt-Right`] (c) {
// let lvl = getHeaderLevel(c)
// if (lvl >= 6) { lvl = 5 }
// setHeaderLine(lvl + 1)
// return false
// },
// [`${CtrlKey}-Alt-Left`] (c) {
// let lvl = getHeaderLevel(c)
// if (lvl <= 1) { lvl = 2 }
// setHeaderLine(lvl - 1)
// return false
// }
// }
// cm.value.setOption('extraKeys', keyBindings)
// this.cm.on('inputRead', this.autocomplete)
// // Handle cursor movement
@ -516,11 +576,11 @@ onMounted(async () => {
// this.cm.on('paste', this.onCmPaste)
// // Render initial preview
processContent(pageStore.content)
nextTick(() => {
cm.value.refresh()
cm.value.focus()
})
// processContent(pageStore.content)
// nextTick(() => {
// cm.value.refresh()
// cm.value.focus()
// })
EVENT_BUS.on('insertAsset', insertAssetClb)
@ -589,6 +649,10 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
// @include until($tablet) {
// height: $editor-height-mobile;
// }
> div {
height: 100%;
}
}
&-type {
writing-mode: vertical-rl;
@ -693,7 +757,7 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
}
&-toolbar {
background-color: $primary;
border-left: 40px solid darken($primary, 5%);
border-left: 50px solid darken($primary, 5%);
color: #FFF;
height: 32px;
}

@ -0,0 +1,621 @@
// Adapted from https://github.com/trofimander/monaco-markdown/blob/master/src/ts/extHostTypes.ts
// by https://github.com/trofimander
// MIT Licensed
// export function values<V = any>(set: Set<V>): V[];
// export function values<K = any, V = any>(map: Map<K, V>): V[];
export function values (forEachable) {
const result = []
forEachable.forEach(value => result.push(value))
return result
}
export class Position {
static Min (...positions) {
if (positions.length === 0) {
throw new TypeError()
}
let result = positions[0]
for (let i = 1; i < positions.length; i++) {
const p = positions[i]
if (p.isBefore(result)) {
result = p
}
}
return result
}
static Max (...positions) {
if (positions.length === 0) {
throw new TypeError()
}
let result = positions[0]
for (let i = 1; i < positions.length; i++) {
const p = positions[i]
if (p.isAfter(result)) {
result = p
}
}
return result
}
static isPosition (other) {
if (!other) {
return false
}
if (other instanceof Position) {
return true
}
const { line, character } = other
if (typeof line === 'number' && typeof character === 'number') {
return true
}
return false
}
get line () {
return this._line
}
get character () {
return this._character
}
constructor (line, character) {
if (line < 0) {
throw new Error('line must be non-negative')
}
if (character < 0) {
throw new Error('character must be non-negative')
}
this._line = line
this._character = character
}
isBefore (other) {
if (this._line < other._line) {
return true
}
if (other._line < this._line) {
return false
}
return this._character < other._character
}
isBeforeOrEqual (other) {
if (this._line < other._line) {
return true
}
if (other._line < this._line) {
return false
}
return this._character <= other._character
}
isAfter (other) {
return !this.isBeforeOrEqual(other)
}
isAfterOrEqual (other) {
return !this.isBefore(other)
}
isEqual (other) {
return this._line === other._line && this._character === other._character
}
compareTo (other) {
if (this._line < other._line) {
return -1
} else if (this._line > other.line) {
return 1
} else {
// equal line
if (this._character < other._character) {
return -1
} else if (this._character > other._character) {
return 1
} else {
// equal line and character
return 0
}
}
}
translate (lineDeltaOrChange, characterDelta = 0) {
if (lineDeltaOrChange === null || characterDelta === null) {
throw new Error()
}
let lineDelta
if (typeof lineDeltaOrChange === 'undefined') {
lineDelta = 0
} else if (typeof lineDeltaOrChange === 'number') {
lineDelta = lineDeltaOrChange
} else {
lineDelta = typeof lineDeltaOrChange.lineDelta === 'number' ? lineDeltaOrChange.lineDelta : 0
characterDelta = typeof lineDeltaOrChange.characterDelta === 'number' ? lineDeltaOrChange.characterDelta : 0
}
if (lineDelta === 0 && characterDelta === 0) {
return this
}
return new Position(this.line + lineDelta, this.character + characterDelta)
}
with (lineOrChange, character = this.character) {
if (lineOrChange === null || character === null) {
throw new Error()
}
let line
if (typeof lineOrChange === 'undefined') {
line = this.line
} else if (typeof lineOrChange === 'number') {
line = lineOrChange
} else {
line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line
character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character
}
if (line === this.line && character === this.character) {
return this
}
return new Position(line, character)
}
toJSON () {
return { line: this.line, character: this.character }
}
}
export class Range {
static isRange (thing) {
if (thing instanceof Range) {
return true
}
if (!thing) {
return false
}
return Position.isPosition(thing.start) && Position.isPosition(thing.end)
}
get start () {
return this._start
}
get end () {
return this._end
}
constructor (startLineOrStart, startColumnOrEnd, endLine, endColumn) {
let start
let end
if (typeof startLineOrStart === 'number' && typeof startColumnOrEnd === 'number' && typeof endLine === 'number' && typeof endColumn === 'number') {
start = new Position(startLineOrStart, startColumnOrEnd)
end = new Position(endLine, endColumn)
} else if (startLineOrStart instanceof Position && startColumnOrEnd instanceof Position) {
start = startLineOrStart
end = startColumnOrEnd
}
if (!start || !end) {
throw new Error('Invalid arguments')
}
if (start.isBefore(end)) {
this._start = start
this._end = end
} else {
this._start = end
this._end = start
}
}
contains (positionOrRange) {
if (positionOrRange instanceof Range) {
return this.contains(positionOrRange._start) &&
this.contains(positionOrRange._end)
} else if (positionOrRange instanceof Position) {
if (positionOrRange.isBefore(this._start)) {
return false
}
if (this._end.isBefore(positionOrRange)) {
return false
}
return true
}
return false
}
isEqual (other) {
return this._start.isEqual(other._start) && this._end.isEqual(other._end)
}
intersection (other) {
const start = Position.Max(other.start, this._start)
const end = Position.Min(other.end, this._end)
if (start.isAfter(end)) {
// this happens when there is no overlap:
// |-----|
// |----|
return undefined
}
return new Range(start, end)
}
union (other) {
if (this.contains(other)) {
return this
} else if (other.contains(this)) {
return other
}
const start = Position.Min(other.start, this._start)
const end = Position.Max(other.end, this.end)
return new Range(start, end)
}
get isEmpty () {
return this._start.isEqual(this._end)
}
get isSingleLine () {
return this._start.line === this._end.line
}
with (startOrChange, end = this.end) {
if (startOrChange === null || end === null) {
throw new Error()
}
let start
if (!startOrChange) {
start = this.start
} else if (Position.isPosition(startOrChange)) {
start = startOrChange
} else {
start = startOrChange.start || this.start
end = startOrChange.end || this.end
}
if (start.isEqual(this._start) && end.isEqual(this.end)) {
return this
}
return new Range(start, end)
}
toJSON () {
return [this.start, this.end]
}
}
export class Selection extends Range {
static isSelection (thing) {
if (thing instanceof Selection) {
return true
}
if (!thing) {
return false
}
return Range.isRange(thing) &&
Position.isPosition(thing.anchor) &&
Position.isPosition(thing.active) &&
typeof thing.isReversed === 'boolean'
}
get anchor () {
return this._anchor
}
get active () {
return this._active
}
constructor (anchorLineOrAnchor, anchorColumnOrActive, activeLine, activeColumn) {
let anchor
let active
if (typeof anchorLineOrAnchor === 'number' && typeof anchorColumnOrActive === 'number' && typeof activeLine === 'number' && typeof activeColumn === 'number') {
anchor = new Position(anchorLineOrAnchor, anchorColumnOrActive)
active = new Position(activeLine, activeColumn)
} else if (anchorLineOrAnchor instanceof Position && anchorColumnOrActive instanceof Position) {
anchor = anchorLineOrAnchor
active = anchorColumnOrActive
}
if (!anchor || !active) {
throw new Error('Invalid arguments')
}
super(anchor, active)
this._anchor = anchor
this._active = active
}
get isReversed () {
return this._anchor === this._end
}
toJSON () {
return {
start: this.start,
end: this.end,
active: this.active,
anchor: this.anchor
}
}
}
export const EndOfLine = {
LF: 1,
CRLF: 2
}
export class TextEdit {
static isTextEdit (thing) {
if (thing instanceof TextEdit) {
return true
}
if (!thing) {
return false
}
return Range.isRange(thing) && typeof thing.newText === 'string'
}
static replace (range, newText) {
return new TextEdit(range, newText)
}
static insert (position, newText) {
return TextEdit.replace(new Range(position, position), newText)
}
static delete (range) {
return TextEdit.replace(range, '')
}
static setEndOfLine (eol) {
const ret = new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), '')
ret.newEol = eol
return ret
}
get range () {
return this._range
}
set range (value) {
if (value && !Range.isRange(value)) {
throw new Error('range')
}
this._range = value
}
get newText () {
return this._newText || ''
}
set newText (value) {
if (value && typeof value !== 'string') {
throw new Error('newText')
}
this._newText = value
}
get newEol () {
return this._newEol
}
set newEol (value) {
if (value && typeof value !== 'number') {
throw new Error('newEol')
}
this._newEol = value
}
constructor (range, newText) {
this.range = range
this._newText = newText
}
toJSON () {
return {
range: this.range,
newText: this.newText,
newEol: this._newEol
}
}
}
export class WorkspaceEdit {
constructor () {
this._edits = []
}
renameFile (from, to, options) {
this._edits.push({ _type: 1, from, to, options })
}
createFile (uri, options) {
this._edits.push({ _type: 1, from: undefined, to: uri, options })
}
deleteFile (uri, options) {
this._edits.push({ _type: 1, from: uri, to: undefined, options })
}
replace (uri, range, newText) {
this._edits.push({ _type: 2, uri, edit: new TextEdit(range, newText) })
}
insert (resource, position, newText) {
this.replace(resource, new Range(position, position), newText)
}
delete (resource, range) {
this.replace(resource, range, '')
}
has (uri) {
for (const edit of this._edits) {
if (edit._type === 2 && edit.uri.toString() === uri.toString()) {
return true
}
}
return false
}
set (uri, edits) {
if (!edits) {
// remove all text edits for `uri`
for (let i = 0; i < this._edits.length; i++) {
const element = this._edits[i]
if (element._type === 2 && element.uri.toString() === uri.toString()) {
this._edits[i] = undefined // will be coalesced down below
}
}
// this._edits = coalesce(this._edits); TODO
} else {
// append edit to the end
for (const edit of edits) {
if (edit) {
this._edits.push({ _type: 2, uri, edit })
}
}
}
}
get (uri) {
const res = []
for (const candidate of this._edits) {
if (candidate._type === 2 && candidate.uri.toString() === uri.toString()) {
res.push(candidate.edit)
}
}
return res
}
entries () {
const textEdits = new Map()
for (const candidate of this._edits) {
if (candidate._type === 2) {
let textEdit = textEdits.get(candidate.uri.toString())
if (!textEdit) {
textEdit = [candidate.uri, []]
textEdits.set(candidate.uri.toString(), textEdit)
}
textEdit[1].push(candidate.edit)
}
}
return values(textEdits)
}
_allEntries () {
const res = []
for (const edit of this._edits) {
if (edit._type === 1) {
res.push([edit.from, edit.to, edit.options])
} else {
res.push([edit.uri, [edit.edit]])
}
}
return res
}
get size () {
return this.entries().length
}
toJSON () {
return this.entries()
}
}
export const TextEditorRevealType = {
Default: 0,
InCenter: 1,
InCenterIfOutsideViewport: 2,
AtTop: 3
}
export const TextEditorSelectionChangeKind = {
Keyboard: 1,
Mouse: 2,
Command: 3
}
export class SnippetString {
static isSnippetString (thing) {
if (thing instanceof SnippetString) {
return true
}
if (!thing) {
return false
}
return typeof thing.value === 'string'
}
static _escape (value) {
return value.replace(/\$|}|\\/g, '\\$&')
}
constructor (value) {
this._tabstop = 1
this.value = value || ''
}
appendText (string) {
this.value += SnippetString._escape(string)
return this
}
appendTabstop (number = this._tabstop++) {
this.value += '$'
this.value += number
return this
}
appendPlaceholder (value, number = this._tabstop++) {
if (typeof value === 'function') {
const nested = new SnippetString()
nested._tabstop = this._tabstop
value(nested)
this._tabstop = nested._tabstop
value = nested.value
} else {
value = SnippetString._escape(value)
}
this.value += '${'
this.value += number
this.value += ':'
this.value += value
this.value += '}'
return this
}
appendVariable (name, defaultValue) {
if (typeof defaultValue === 'function') {
const nested = new SnippetString()
nested._tabstop = this._tabstop
defaultValue(nested)
this._tabstop = nested._tabstop
defaultValue = nested.value
} else if (typeof defaultValue === 'string') {
defaultValue = defaultValue.replace(/\$|}/g, '\\$&')
}
this.value += '${'
this.value += name
if (defaultValue) {
this.value += ':'
this.value += defaultValue
}
this.value += '}'
return this
}
}
Loading…
Cancel
Save