feat: source code mode

This commit is contained in:
Jocs 2018-03-03 02:30:06 +08:00
parent 2ff0137b97
commit a77cf47ea0
14 changed files with 314 additions and 69 deletions

View File

@ -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.

View File

@ -1,15 +1 @@
wow
owoww
owow
wowowo wowowow

View File

@ -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

View 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

View File

@ -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 {

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -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 => {

View File

@ -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>

View File

@ -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;

View File

@ -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)

View 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>

View File

@ -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: {

View File

@ -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)