mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 00:01:19 +08:00
feat: source code mode
This commit is contained in:
parent
2ff0137b97
commit
a77cf47ea0
@ -4,6 +4,8 @@
|
||||
|
||||
- Add Typewriter Mode, The current line will always in the center of the document. If you change the current line, it will be auto scroll to the new line.
|
||||
|
||||
- Add Focus Mode, the current paragraph's will be focused.
|
||||
|
||||
**Optimization**
|
||||
|
||||
- Optimize the display of path name and file name in title bar.
|
||||
|
@ -1,15 +1 @@
|
||||
wow
|
||||
|
||||
|
||||
|
||||
owoww
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
owow
|
||||
|
||||
|
||||
|
||||
|
||||
wowowo wowowow
|
||||
|
@ -1,14 +1,17 @@
|
||||
import 'codemirror/addon/edit/closebrackets'
|
||||
import 'codemirror/addon/edit/closetag'
|
||||
import 'codemirror/addon/selection/active-line'
|
||||
import 'codemirror/mode/meta'
|
||||
import codeMirror from 'codemirror/lib/codemirror'
|
||||
|
||||
import loadmode from './loadmode'
|
||||
import overlayMode from './overlayMode'
|
||||
import languages from './modes'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import './index.css'
|
||||
|
||||
loadmode(codeMirror)
|
||||
overlayMode(codeMirror)
|
||||
window.CodeMirror = codeMirror
|
||||
|
||||
const modes = codeMirror.modeInfo
|
||||
|
98
src/editor/codeMirror/overlayMode.js
Normal file
98
src/editor/codeMirror/overlayMode.js
Normal file
@ -0,0 +1,98 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// Utility function that allows modes to be combined. The mode given
|
||||
// as the base argument takes care of most of the normal mode
|
||||
// functionality, but a second (typically simple) mode is used, which
|
||||
// can override the style of text. Both modes get to parse all of the
|
||||
// text, but when both assign a non-null style to a piece of code, the
|
||||
// overlay wins, unless the combine argument was true and not overridden,
|
||||
// or state.overlay.combineTokens was true, in which case the styles are
|
||||
// combined.
|
||||
|
||||
const overlayMode = CodeMirror => {
|
||||
CodeMirror.overlayMode = function (base, overlay, combine) {
|
||||
return {
|
||||
startState () {
|
||||
return {
|
||||
base: CodeMirror.startState(base),
|
||||
overlay: CodeMirror.startState(overlay),
|
||||
basePos: 0,
|
||||
baseCur: null,
|
||||
overlayPos: 0,
|
||||
overlayCur: null,
|
||||
streamSeen: null
|
||||
}
|
||||
},
|
||||
|
||||
copyState (state) {
|
||||
return {
|
||||
base: CodeMirror.copyState(base, state.base),
|
||||
overlay: CodeMirror.copyState(overlay, state.overlay),
|
||||
basePos: state.basePos,
|
||||
baseCur: null,
|
||||
overlayPos: state.overlayPos,
|
||||
overlayCur: null
|
||||
}
|
||||
},
|
||||
|
||||
token (stream, state) {
|
||||
if (stream !== state.streamSeen ||
|
||||
Math.min(state.basePos, state.overlayPos) < stream.start) {
|
||||
state.streamSeen = stream
|
||||
state.basePos = state.overlayPos = stream.start
|
||||
}
|
||||
|
||||
if (stream.start === state.basePos) {
|
||||
state.baseCur = base.token(stream, state.base)
|
||||
state.basePos = stream.pos
|
||||
}
|
||||
|
||||
if (stream.start === state.overlayPos) {
|
||||
stream.pos = stream.start
|
||||
state.overlayCur = overlay.token(stream, state.overlay)
|
||||
state.overlayPos = stream.pos
|
||||
}
|
||||
|
||||
stream.pos = Math.min(state.basePos, state.overlayPos)
|
||||
|
||||
// state.overlay.combineTokens always takes precedence over combine,
|
||||
// unless set to null
|
||||
if (state.overlayCur === null) {
|
||||
return state.baseCur
|
||||
} else if (
|
||||
(state.baseCur !== null &&
|
||||
state.overlay.combineTokens) ||
|
||||
(combine && state.overlay.combineTokens === null)
|
||||
) {
|
||||
return state.baseCur + ' ' + state.overlayCur
|
||||
} else return state.overlayCur
|
||||
},
|
||||
|
||||
indent: base.indent && function (state, textAfter) {
|
||||
return base.indent(state.base, textAfter)
|
||||
},
|
||||
|
||||
electricChars: base.electricChars,
|
||||
|
||||
innerMode (state) {
|
||||
return {
|
||||
state: state.base,
|
||||
mode: base
|
||||
}
|
||||
},
|
||||
|
||||
blankLine (state) {
|
||||
var baseToken, overlayToken
|
||||
if (base.blankLine) baseToken = base.blankLine(state.base)
|
||||
if (overlay.blankLine) overlayToken = overlay.blankLine(state.overlay)
|
||||
|
||||
return overlayToken == null
|
||||
? baseToken
|
||||
: (combine && baseToken != null ? baseToken + ' ' + overlayToken : overlayToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default overlayMode
|
@ -135,8 +135,8 @@ li.ag-task-list-item > input[type=checkbox] {
|
||||
height: inherit;
|
||||
margin: 4px 0px 0px;
|
||||
top: 1px;
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
left: -20px;
|
||||
}
|
||||
|
||||
@ -146,8 +146,8 @@ li.ag-task-list-item > input.ag-checkbox-checked ~ p {
|
||||
|
||||
li.ag-task-list-item > input[type=checkbox]::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
border: 2px solid #606266;
|
||||
@ -174,7 +174,7 @@ li.ag-task-list-item > input.ag-checkbox-checked::after {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
li p .ag-hide:first-child {
|
||||
|
@ -390,10 +390,12 @@ class Aganippe {
|
||||
return this.contentState.wordCount()
|
||||
}
|
||||
|
||||
setMarkdown (text) {
|
||||
// if text is blank, dont need to import markdown
|
||||
if (!text.trim()) return
|
||||
this.contentState.importMarkdown(text)
|
||||
setMarkdown (markdown, cursor) {
|
||||
// if markdown is blank, dont need to import markdown
|
||||
if (!markdown.trim()) return
|
||||
this.contentState.importMarkdown(markdown)
|
||||
this.contentState.importCursor(cursor)
|
||||
this.contentState.render()
|
||||
this.dispatchChange()
|
||||
}
|
||||
|
||||
|
@ -226,21 +226,33 @@ const importRegister = ContentState => {
|
||||
return this.getStateFragment(markdown)
|
||||
}
|
||||
|
||||
ContentState.prototype.importCursor = function (cursor) {
|
||||
// set cursor
|
||||
if (cursor) {
|
||||
// TODO for codeMirror cursor to aganippe cursor
|
||||
const lastBlock = this.getLastBlock()
|
||||
const key = lastBlock.key
|
||||
const offset = lastBlock.text.length
|
||||
this.cursor = {
|
||||
start: { key, offset },
|
||||
end: { key, offset }
|
||||
}
|
||||
} else {
|
||||
const lastBlock = this.getLastBlock()
|
||||
const key = lastBlock.key
|
||||
const offset = lastBlock.text.length
|
||||
this.cursor = {
|
||||
start: { key, offset },
|
||||
end: { key, offset }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.importMarkdown = function (markdown) {
|
||||
// empty the blocks and codeBlocks
|
||||
this.keys = new Set()
|
||||
this.codeBlocks = new Map()
|
||||
this.blocks = this.getStateFragment(markdown)
|
||||
// set cursor
|
||||
const lastBlock = this.getLastBlock()
|
||||
const key = lastBlock.key
|
||||
const offset = lastBlock.text.length
|
||||
this.cursor = {
|
||||
start: { key, offset },
|
||||
end: { key, offset }
|
||||
}
|
||||
// re-render
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,15 @@ const HASH = {
|
||||
export const view = (win, item, type) => {
|
||||
const { checked } = item
|
||||
win.webContents.send('AGANI::view', { type, checked })
|
||||
|
||||
if (type === 'sourceCode') {
|
||||
const viewMenuItem = getMenuItem('View')
|
||||
viewMenuItem.submenu.items.forEach(item => {
|
||||
if (/typewriter|focus/.test(HASH[item.label])) {
|
||||
item.enabled = !checked
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.on('AGANI::ask-for-mode', e => {
|
||||
|
@ -9,8 +9,18 @@
|
||||
:typewriter="typewriter"
|
||||
:focus="focus"
|
||||
:source-code="sourceCode"
|
||||
:markdown="markdown"
|
||||
:cursor="cursor"
|
||||
v-if="!sourceCode"
|
||||
></editor>
|
||||
<search></search>
|
||||
<source-code
|
||||
v-else
|
||||
:markdown="markdown"
|
||||
:cursor="cursor"
|
||||
></source-code>
|
||||
<search
|
||||
v-if="!sourceCode"
|
||||
></search>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -18,6 +28,7 @@
|
||||
import Editor from '@/components/editor'
|
||||
import TitleBar from '@/components/titleBar'
|
||||
import Search from '@/components/search'
|
||||
import SourceCode from '@/components/sourceCode'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
@ -25,13 +36,14 @@
|
||||
components: {
|
||||
Editor,
|
||||
TitleBar,
|
||||
Search
|
||||
Search,
|
||||
SourceCode
|
||||
},
|
||||
data () {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['pathname', 'windowActive', 'wordCount', 'typewriter', 'focus', 'sourceCode'])
|
||||
...mapState(['pathname', 'windowActive', 'wordCount', 'typewriter', 'focus', 'sourceCode', 'markdown', 'cursor'])
|
||||
},
|
||||
created () {
|
||||
const { dispatch } = this.$store
|
||||
@ -56,4 +68,10 @@
|
||||
.editor-container {
|
||||
padding-top: 22px;
|
||||
}
|
||||
.editor-container .hide {
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="editor-wrapper"
|
||||
:class="{ 'typewriter': typewriter, 'focus': focus, 'source-code': sourceCode }"
|
||||
:class="{ 'typewriter': typewriter, 'focus': focus, 'source': sourceCode }"
|
||||
>
|
||||
<div
|
||||
ref="editor"
|
||||
@ -81,7 +81,9 @@
|
||||
sourceCode: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
markdown: String,
|
||||
cursor: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@ -114,8 +116,14 @@
|
||||
this.$nextTick(() => {
|
||||
const ele = this.$refs.editor
|
||||
this.editor = new Aganippe(ele)
|
||||
const { container } = this.editor
|
||||
const { markdown } = this
|
||||
|
||||
bus.$on('file-loaded', this.handleFileLoaded)
|
||||
if (markdown.trim()) {
|
||||
this.setMarkdownToEditor(markdown)
|
||||
}
|
||||
|
||||
bus.$on('file-loaded', this.setMarkdownToEditor)
|
||||
bus.$on('undo', () => this.editor.undo())
|
||||
bus.$on('redo', () => this.editor.redo())
|
||||
bus.$on('export', this.handleExport)
|
||||
@ -125,8 +133,6 @@
|
||||
bus.$on('replaceValue', this.handReplace)
|
||||
bus.$on('find', this.handleFind)
|
||||
|
||||
const { container } = this.editor
|
||||
|
||||
this.editor.on('change', (markdown, wordCount) => {
|
||||
this.$store.dispatch('SAVE_FILE', { markdown, wordCount })
|
||||
})
|
||||
@ -210,16 +216,23 @@
|
||||
this.dialogTableVisible = false
|
||||
this.editor && this.editor.createTable(this.tableChecker)
|
||||
},
|
||||
handleFileLoaded (file) {
|
||||
this.editor && this.editor.setMarkdown(file)
|
||||
setMarkdownToEditor (markdown) {
|
||||
const { cursor } = this
|
||||
this.editor && this.editor.setMarkdown(markdown, cursor)
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
bus.$off('file-loaded', this.handleFileLoaded)
|
||||
bus.$off('export-styled-html', this.handleExport('styledHtml'))
|
||||
bus.$off('file-loaded', this.setMarkdownToEditor)
|
||||
bus.$off('undo', () => this.editor.undo())
|
||||
bus.$off('redo', () => this.editor.redo())
|
||||
bus.$off('export', this.handleExport)
|
||||
bus.$off('paragraph', this.handleEditParagraph)
|
||||
bus.$off('format', this.handleInlineFormat)
|
||||
bus.$off('searchValue', this.handleSearch)
|
||||
bus.$off('replaceValue', this.handReplace)
|
||||
bus.$off('find', this.handleFind)
|
||||
|
||||
// this.editor.destroy()
|
||||
this.editor = null
|
||||
}
|
||||
}
|
||||
@ -231,6 +244,12 @@
|
||||
.editor-wrapper {
|
||||
height: calc(100vh - 22px);
|
||||
}
|
||||
.editor-wrapper.source {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
left: -10000px;
|
||||
opacity: 0;
|
||||
}
|
||||
.editor-component {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
@ -136,31 +136,39 @@
|
||||
}
|
||||
},
|
||||
created () {
|
||||
bus.$on('find', () => {
|
||||
bus.$on('find', this.listenFind)
|
||||
bus.$on('replace', this.listenReplace)
|
||||
bus.$on('findNext', this.listenFindNext)
|
||||
bus.$on('findPrev', this.listenFindPrev)
|
||||
document.addEventListener('click', this.docClick)
|
||||
document.addEventListener('keyup', this.docKeyup)
|
||||
},
|
||||
beforeDestroy () {
|
||||
bus.$off('find', this.listenFind)
|
||||
bus.$off('replace', this.listenReplace)
|
||||
bus.$off('findNext', this.listenFindNext)
|
||||
bus.$off('findPrev', this.listenFindPrev)
|
||||
document.removeEventListener('click', this.docClick)
|
||||
document.removeEventListener('keyup', this.docKeyup)
|
||||
},
|
||||
methods: {
|
||||
listenFind () {
|
||||
this.showSearch = true
|
||||
this.type = 'search'
|
||||
this.$nextTick(() => {
|
||||
this.$refs.search.focus()
|
||||
})
|
||||
})
|
||||
bus.$on('replace', () => {
|
||||
},
|
||||
listenReplace () {
|
||||
this.showSearch = true
|
||||
this.type = 'replace'
|
||||
})
|
||||
bus.$on('findNext', () => {
|
||||
},
|
||||
listenFindNext () {
|
||||
this.find('next')
|
||||
})
|
||||
bus.$on('findPrev', () => {
|
||||
},
|
||||
listenFindPrev () {
|
||||
this.find('prev')
|
||||
})
|
||||
document.addEventListener('click', this.docClick)
|
||||
document.addEventListener('keyup', this.docKeyup)
|
||||
},
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('click', this.docClick)
|
||||
document.removeEventListener('keyup', this.docKeyup)
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
docKeyup (event) {
|
||||
if (event.key === 'Escape') {
|
||||
this.emitSearch(true)
|
||||
|
86
src/renderer/components/sourceCode.vue
Normal file
86
src/renderer/components/sourceCode.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="source-code" ref="sourceCode">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import codeMirror, { setMode, setCursorAtLastLine } from '../../editor/codeMirror'
|
||||
import ContentState from '../../editor/contentState'
|
||||
import bus from '../bus'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
markdown: String,
|
||||
cursor: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
contentState: null,
|
||||
editor: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$nextTick(() => {
|
||||
this.contentState = new ContentState()
|
||||
const container = this.$refs.sourceCode
|
||||
const editor = this.editor = codeMirror(container, {
|
||||
value: this.markdown || '',
|
||||
lineNumbers: true,
|
||||
autofocus: true,
|
||||
lineWrapping: true,
|
||||
styleActiveLine: true,
|
||||
lineNumberFormatter (line) {
|
||||
if (line % 10 === 0 || line === 1) {
|
||||
return line
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
})
|
||||
bus.$on('file-loaded', this.setMarkdown)
|
||||
|
||||
editor.on('cursorActivity', (cm, event) => {
|
||||
const cursor = cm.getCursor()
|
||||
const markdown = cm.getValue()
|
||||
// get word count
|
||||
this.contentState.importMarkdown(markdown, cursor)
|
||||
const wordCount = this.contentState.wordCount()
|
||||
this.$store.dispatch('SAVE_FILE', { markdown, cursor, wordCount })
|
||||
})
|
||||
|
||||
setMode(editor, 'markdown')
|
||||
this.setMarkdown(this.markdown || '')
|
||||
})
|
||||
},
|
||||
beforeDestory () {
|
||||
bus.$off('file-loaded', this.setMarkdown)
|
||||
},
|
||||
methods: {
|
||||
setMarkdown (markdown) {
|
||||
const { editor } = this
|
||||
this.editor.setValue(markdown)
|
||||
setCursorAtLastLine(editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.source-code {
|
||||
height: calc(100vh - 22px);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
.source-code .CodeMirror {
|
||||
margin: 50px auto;
|
||||
max-width: 860px;
|
||||
}
|
||||
.source-code .CodeMirror-gutters {
|
||||
border-right: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
.source-code .CodeMirror-activeline-background,
|
||||
.source-code .CodeMirror-activeline-gutter {
|
||||
background: #F2F6FC;
|
||||
}
|
||||
</style>
|
@ -34,6 +34,7 @@
|
||||
}
|
||||
},
|
||||
props: {
|
||||
filename: String,
|
||||
pathname: String,
|
||||
active: Boolean,
|
||||
wordCount: Object
|
||||
@ -42,10 +43,6 @@
|
||||
paths () {
|
||||
const pathnameToken = this.pathname.split('/').filter(i => i)
|
||||
return pathnameToken.slice(0, pathnameToken.length - 1).slice(-3)
|
||||
},
|
||||
filename () {
|
||||
const pathnameToken = this.pathname.split('/').filter(i => i)
|
||||
return pathnameToken.pop()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -12,9 +12,10 @@ const state = {
|
||||
typewriter: false, // typewriter mode
|
||||
focus: false, // focus mode
|
||||
sourceCode: false, // source code mode
|
||||
pathname: 'Untitled - unsaved',
|
||||
pathname: '',
|
||||
isSaved: true,
|
||||
markdown: '',
|
||||
cursor: null,
|
||||
windowActive: true,
|
||||
wordCount: {
|
||||
paragraph: 0,
|
||||
@ -49,6 +50,9 @@ const mutations = {
|
||||
},
|
||||
SET_WORD_COUNT (state, wordCount) {
|
||||
state.wordCount = wordCount
|
||||
},
|
||||
SET_CURSOR (state, cursor) {
|
||||
state.cursor = cursor
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,9 +123,10 @@ const actions = {
|
||||
const { filename, pathname } = state
|
||||
ipcRenderer.send('AGANI::response-export', { type, content, filename, pathname })
|
||||
},
|
||||
SAVE_FILE ({ commit, state }, { markdown, wordCount }) {
|
||||
SAVE_FILE ({ commit, state }, { markdown, wordCount, cursor }) {
|
||||
commit('SET_MARKDOWN', markdown)
|
||||
commit('SET_WORD_COUNT', wordCount)
|
||||
if (wordCount) commit('SET_WORD_COUNT', wordCount)
|
||||
if (cursor) commit('SET_CURSOR', cursor)
|
||||
const { pathname } = state
|
||||
if (pathname) {
|
||||
commit('SET_STATUS', true)
|
||||
|
Loading…
Reference in New Issue
Block a user