marktext/src/renderer/components/editorWithTabs/sourceCode.vue.orig
Mitch Capper b6754741f9 Remember the file's scroll position as long as it stays open
By default returns the user to the same scroll position on a document where they left off (except if changed to code editing before switching back, or vice versa). Only saved in memory could be added to future session saves for #998.
2022-03-16 15:53:27 -07:00

390 lines
12 KiB
Vue

<template>
<div
class="source-code"
ref="sourceCode"
>
</div>
</template>
<script>
import codeMirror, { setMode, setCursorAtLastLine, setTextDirection } from '../../codeMirror'
import { wordCount as getWordCount } from 'muya/lib/utils'
import { mapState } from 'vuex'
import { adjustCursor, animatedScrollTo } from '../../util'
import bus from '../../bus'
import { oneDarkThemes, railscastsThemes } from '@/config'
const STANDAR_Y = 320
export default {
props: {
markdown: String,
cursor: Object,
textDirection: {
type: String,
required: true
}
},
computed: {
...mapState({
theme: state => state.preferences.theme,
sourceCode: state => state.preferences.sourceCode,
currentTab: state => state.editor.currentFile
})
},
data () {
return {
contentState: null,
editor: null,
commitTimer: null,
viewDestroyed: false,
tabId: null
}
},
watch: {
currentTab: function (value, oldValue) {
if (! this.sourceCode)
return;
//Don't need to worry about setting the line on the oldValue already did in prepareTabSwitch
if (value && value !== oldValue) {
if (value.firstViewportVisibleItem && value.firstViewportVisibleItem.startsWith("S")){
let line = value.firstViewportVisibleItem.substring(1);
this.$nextTick(() => {
this.scrollToLineNumberInViewport(line, 15,true);
});
}
}
},
textDirection: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
setTextDirection(editor, value)
}
}
},
created () {
this.$nextTick(() => {
// TODO: Should we load markdown from the tab or mapped vue property?
const { id } = this.currentTab
const { markdown = '', theme, cursor, textDirection } = this
const container = this.$refs.sourceCode
const codeMirrorConfig = {
value: markdown,
lineNumbers: true,
autofocus: true,
lineWrapping: true,
styleActiveLine: true,
direction: textDirection,
// The amount of updates needed when scrolling. Settings this to >Infinity< or use CSS
// >height: auto< result in bad performance because the whole document is always rendered.
// Since we are using >height: auto< setting this to >Infinity< to fix #171. The best
// solution would be to set a fixed height like in #791 but then the scrollbar is not on
// the right side. Please also see CodeMirror#1104.
viewportMargin: Infinity,
lineNumberFormatter (line) {
if (line % 10 === 0 || line === 1) {
return line
} else {
return ''
}
}
}
// Set theme
if (railscastsThemes.includes(theme)) {
codeMirrorConfig.theme = 'railscasts'
} else if (oneDarkThemes.includes(theme)) {
codeMirrorConfig.theme = 'one-dark'
}
// Init CodeMirror
const editor = this.editor = codeMirror(container, codeMirrorConfig)
bus.$on('file-loaded', this.handleFileChange)
bus.$on('invalidate-image-cache', this.handleInvalidateImageCache)
bus.$on('file-changed', this.handleFileChange)
bus.$on('selectAll', this.handleSelectAll)
bus.$on('image-action', this.handleImageAction)
bus.$on('undo', this.handleUndo)
bus.$on('redo', this.handleRedo)
bus.$on('find', this.handleFind)
bus.$on('replace', this.handleReplace)
bus.$on('findNext', this.handleFindNext)
bus.$on('findPrev', this.handleFindPrev)
document.addEventListener('click', this.docClick)
document.addEventListener('keyup', this.docKeyup)
setMode(editor, 'markdown')
this.listenChange()
// NOTE: Cursor may be not null but the inner values are.
if (cursor && cursor.anchor && cursor.focus) {
const { anchor, focus } = cursor
editor.setSelection(anchor, focus, { scroll: true }) // Scroll the focus into view.
} else {
setCursorAtLastLine(editor)
}
this.tabId = id
})
},
beforeDestroy () {
// NOTE: Clear timer and manually commit changes. After mode switching and cleanup may follow
// further key inputs, so ignore all inputs.
this.viewDestroyed = true
if (this.commitTimer) clearTimeout(this.commitTimer)
bus.$off('file-loaded', this.handleFileChange)
bus.$off('invalidate-image-cache', this.handleInvalidateImageCache)
bus.$off('file-changed', this.handleFileChange)
bus.$off('selectAll', this.handleSelectAll)
bus.$off('image-action', this.handleImageAction)
bus.$off('undo', this.handleUndo)
bus.$off('redo', this.handleRedo)
bus.$off('find', this.handleFind)
bus.$off('replace', this.handleReplace)
bus.$off('findNext', this.handleFindNext)
bus.$off('findPrev', this.handleFindPrev)
document.removeEventListener('click', this.docClick)
document.removeEventListener('keyup', this.docKeyup)
const { editor } = this
const { cursor, markdown } = this.getMarkdownAndCursor(editor)
bus.$emit('file-changed', { id: this.tabId, markdown, cursor, renderCursor: true })
},
methods: {
handleImageAction ({ id, result, alt }) {
const { editor } = this
const value = editor.getValue()
const focus = editor.getCursor('focus')
const anchor = editor.getCursor('anchor')
const lines = value.split('\n')
const index = lines.findIndex(line => line.indexOf(id) > 0)
if (index > -1) {
const oldLine = lines[index]
lines[index] = oldLine.replace(new RegExp(`!\\[${id}\\]\\(.*\\)`), `![${alt}](${result})`)
const newValue = lines.join('\n')
editor.setValue(newValue)
const match = /(!\[.*\]\(.*\))/.exec(oldLine)
if (!match) {
// User maybe delete `![]()` structure, and the match is null.
return
}
const range = {
start: match.index,
end: match.index + match[1].length
}
const delta = alt.length + result.length + 5 - match[1].length
const adjust = pointer => {
if (!pointer) {
return
}
if (pointer.line !== index) {
return
}
if (pointer.ch <= range.start) {
// do nothing.
} else if (pointer.ch > range.start && pointer.ch < range.end) {
pointer.ch = range.start + alt.length + result.length + 5
} else {
pointer.ch += delta
}
}
adjust(focus)
adjust(anchor)
if (focus && anchor) {
editor.setSelection(anchor, focus, { scroll: true })
} else {
setCursorAtLastLine()
}
}
},
listenChange () {
const { editor } = this
editor.on('cursorActivity', cm => {
const { cursor, markdown } = this.getMarkdownAndCursor(cm)
// Attention: the cursor may be `{focus: null, anchor: null}` when press `backspace`
const wordCount = getWordCount(markdown)
if (this.commitTimer) clearTimeout(this.commitTimer)
this.commitTimer = setTimeout(() => {
// See "beforeDestroy" note
if (!this.viewDestroyed) {
if (this.tabId) {
this.$store.dispatch('LISTEN_FOR_CONTENT_CHANGE', { id: this.tabId, markdown, wordCount, cursor })
} else {
// This may occur during tab switching but should not occur otherwise.
console.warn('LISTEN_FOR_CONTENT_CHANGE: Cannot commit changes because not tab id was set!')
}
}
}, 1000)
})
},
// Another tab was selected - only listen to get changes but don't set history or other things.
handleFileChange ({ id, markdown, cursor }) {
this.prepareTabSwitch()
const { editor } = this
if (typeof markdown === 'string') {
editor.setValue(markdown)
}
// Cursor is null when loading a file or creating a new tab in source code mode.
if (cursor) {
const { anchor, focus } = cursor
editor.setSelection(anchor, focus, { scroll: true }) // Scroll the focus into view.
} else {
setCursorAtLastLine(editor)
}
this.tabId = id
},
// Get markdown and cursor from CodeMirror.
getMarkdownAndCursor (cm) {
let focus = cm.getCursor('head')
let anchor = cm.getCursor('anchor')
const markdown = cm.getValue()
const convertToMuyaCursor = cursor => {
const line = cm.getLine(cursor.line)
const preLine = cm.getLine(cursor.line - 1)
const nextLine = cm.getLine(cursor.line + 1)
return adjustCursor(cursor, preLine, line, nextLine)
}
anchor = convertToMuyaCursor(anchor) // Selection start as Muya cursor
focus = convertToMuyaCursor(focus) // Selection end as Muya cursor
// Normalize cursor that `anchor` is always before `focus` because
// this is the expected behavior in Muya.
if (anchor && focus && anchor.line > focus.line) {
const tmpCursor = focus
focus = anchor
anchor = tmpCursor
}
return { cursor: { focus, anchor }, markdown }
},
// Commit changes from old tab. Problem: tab was already switched, so commit changes with old tab id.
prepareTabSwitch () {
let firstViewportVisibleItem = "S" + this.getFirstLineNumberInViewport();
if (this.sourceCode == false)//this shouldn't happen
firstViewportVisibleItem = undefined;
if (this.commitTimer) clearTimeout(this.commitTimer)
if (this.tabId) {
const { editor } = this
const { cursor, markdown } = this.getMarkdownAndCursor(editor)
this.$store.dispatch('LISTEN_FOR_CONTENT_CHANGE', { id: this.tabId, markdown, cursor, firstViewportVisibleItem })
this.tabId = null // invalidate tab id
}
},
<<<<<<< HEAD
scrollToLineNumberInViewport(line, duration=300, dontAddStandardHeadroom=false){
let pos = this.editor.charCoords({line:line,ch:0},"local").top;
const DURATION = duration;
if (! dontAddStandardHeadroom)
pos -= STANDAR_Y;
animatedScrollTo(this.$el, pos, DURATION)
},
getFirstLineNumberInViewport(){
return this.editor.coordsChar({left:0,top:this.$el.scrollTop},"local").line;
},
=======
handleUndo () {
if (this.editor && this.sourceCode) {
this.editor.execCommand('undo')
}
},
handleRedo () {
if (this.editor && this.sourceCode) {
this.editor.execCommand('redo')
}
},
docKeyup (event) {
if (this.editor && this.sourceCode && event.key === 'Escape') {
this.editor.execCommand('clearSearch')
}
},
docClick () {
if (this.editor && this.sourceCode)
this.editor.execCommand('clearSearch')
},
handleFind () {
if (this.editor && this.sourceCode) {
this.editor.execCommand('findPersistent')
}
},
handleReplace () {
if (this.editor && this.sourceCode) {
this.editor.execCommand('replace')
}
},
handleFindNext () {
if (this.editor && this.sourceCode) {
this.editor.execCommand('findNext')
}
},
handleFindPrev () {
if (this.editor && this.sourceCode) {
this.editor.execCommand('findPrev')
}
},
>>>>>>> sourcecode_editor_find_undo_redo_fixes
handleSelectAll () {
if (!this.sourceCode) {
return
}
const { editor } = this
if (editor && editor.hasFocus()) {
this.editor.execCommand('selectAll')
} else {
const activeElement = document.activeElement
const nodeName = activeElement.nodeName
if (nodeName === 'INPUT' || nodeName === 'TEXTAREA') {
activeElement.select()
}
}
},
handleInvalidateImageCache () {
if (this.editor) {
this.editor.invalidateImageCache()
}
}
}
}
</script>
<style>
.source-code {
height: calc(100vh - var(--titleBarHeight));
box-sizing: border-box;
overflow: auto;
}
.source-code .CodeMirror {
height: auto;
margin: 50px auto;
max-width: var(--editorAreaWidth);
background: transparent;
}
.source-code .CodeMirror-gutters {
border-right: none;
background-color: transparent;
}
.source-code .CodeMirror-activeline-background,
.source-code .CodeMirror-activeline-gutter {
background: var(--floatHoverColor);
}
</style>