fix: source code mode tab switching (#881)

This commit is contained in:
Felix Häusler 2019-04-06 08:50:40 +02:00 committed by Ran Luo
parent e78bc8036c
commit d12f4fab19
8 changed files with 178 additions and 64 deletions

View File

@ -41,6 +41,7 @@ foo<section>bar</section>zar
- Add new themes: Ulysses Light, Graphite Light, Material Dark and One Dark.
- Watch file changed in tabs and show a notice(autoSave is `false`) or update the file(autoSave is `true`)
- Support input inline Ruby charactors as raw html (#257)
- Added unsaved tab indicator
**:butterfly:Optimization**
@ -109,6 +110,8 @@ foo<section>bar</section>zar
- Fixed bug when combine pre list and next list into one when inline update #707
- Fix renderer error when selection in sidebar (#625)
- Fixed list parse error [more info](https://github.com/marktext/marktext/issues/831#issuecomment-477719256)
- Fixed source code mode tab switching
- Fixed source code mode to preview switching
### 0.13.65

View File

@ -218,6 +218,7 @@ class AppWindow {
newTab (win, filePath) {
this.watcher.watch(win, filePath, 'file')
loadMarkdownFile(filePath).then(rawDocument => {
appMenu.addRecentlyUsedDocument(filePath)
newTab(win, rawDocument)
}).catch(err => {
// TODO: Handle error --> create a end-user error handler.

View File

@ -28,7 +28,7 @@ export const validEmoji = text => {
*/
export const checkEditEmoji = node => {
if (node.classList.contains(CLASS_OR_ID['AG_EMOJI_MARKED_TEXT'])) {
if (node && node.classList.contains(CLASS_OR_ID['AG_EMOJI_MARKED_TEXT'])) {
return node
}
return false

View File

@ -59,6 +59,10 @@
background: #4d78cc !important;
}
.drop-container.active {
border: 1px dashed #4d78cc !important;
}
.title-bar .frameless-titlebar-button > div > svg {
fill: #ffffff;
}
@ -84,6 +88,8 @@
.side-bar-toc .no-data svg {
fill: #4d78cc !important;
}
.recent-files-projects a,
.open-project a {
color: #9da5b4 !important;
border: 1px solid #181a1f !important;
@ -91,6 +97,11 @@
background-image: linear-gradient(#3a3f4b, #353b45) !important;
box-shadow: none !important;
}
.recent-files-projects a:hover,
.open-project a:hover {
color: #d7dae0 !important;
background-image: linear-gradient(#3e4451, #3a3f4b) !important;
}
.editor-tabs {
border-bottom: 1px solid #181a1f;
@ -111,6 +122,9 @@
height: auto !important;
background: #4d78cc !important;
}
.tabs-container svg.close-icon #unsaved-circle-icon {
fill: #4d78cc;
}
:not(pre) > code[class*="language-"],
pre.ag-paragraph {

View File

@ -261,7 +261,8 @@
})
this.editor.on('change', changes => {
this.$store.dispatch('LISTEN_FOR_CONTENT_CHANGE', changes)
// WORKAROUND: "id: 'muya'"
this.$store.dispatch('LISTEN_FOR_CONTENT_CHANGE', Object.assign(changes, { id: 'muya' }))
})
this.editor.on('format-click', ({ event, formatType, data }) => {
@ -445,7 +446,7 @@
},
// listen for `open-single-file` event, it will call this method only when open a new file.
setMarkdownToEditor (markdown) {
setMarkdownToEditor ({id, markdown}) {
const { editor } = this
if (editor) {
editor.clearHistory()
@ -455,7 +456,7 @@
},
// listen for markdown change form source mode or change tabs etc
handleMarkdownChange ({ markdown, cursor, renderCursor, history }) {
handleMarkdownChange ({ id, markdown, cursor, renderCursor, history }) {
const { editor } = this
if (editor) {
if (history) {
@ -488,12 +489,15 @@
bus.$off('undo', this.handleUndo)
bus.$off('redo', this.handleRedo)
bus.$off('export', this.handleExport)
bus.$off('print-service-clearup', this.handlePrintServiceClearup)
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)
bus.$off('dotu-select', this.handleSelect)
bus.$off('insert-image', this.handleSelect)
bus.$off('image-uploaded', this.handleUploadedImage)
bus.$off('file-changed', this.handleMarkdownChange)
bus.$off('editor-blur', this.blurEditor)
bus.$off('image-auto-path', this.handleImagePath)
bus.$off('copyAsMarkdown', this.handleCopyPaste)

View File

@ -26,14 +26,18 @@
computed: {
...mapState({
'theme': state => state.preferences.theme
'theme': state => state.preferences.theme,
'currentTab': state => state.editor.currentFile,
})
},
data () {
return {
contentState: null,
editor: null
editor: null,
commitTimer: null,
viewDestroyed: false,
tabId: null
}
},
@ -48,6 +52,8 @@
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 = {
@ -71,6 +77,7 @@
codeMirrorConfig.theme = 'one-dark'
}
const editor = this.editor = codeMirror(container, codeMirrorConfig)
bus.$on('file-loaded', this.setMarkdown)
bus.$on('file-changed', this.handleMarkdownChange)
bus.$on('dotu-select', this.handleSelectDoutu)
@ -82,13 +89,22 @@
} 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.setMarkdown)
bus.$off('file-changed', this.handleMarkdownChange)
bus.$off('dotu-select', this.handleSelectDoutu)
const { markdown, cursor } = this
bus.$emit('file-changed', { markdown, cursor, renderCursor: true })
const { editor } = this
const { cursor, markdown } = this.getMarkdownAndCursor(editor)
bus.$emit('file-changed', { id: this.tabId, markdown, cursor, renderCursor: true })
},
methods: {
handleSelectDoutu (url) {
@ -99,29 +115,37 @@
},
listenChange () {
const { editor } = this
let timer = null
editor.on('cursorActivity', (cm, event) => {
let cursor = cm.getCursor()
const markdown = cm.getValue()
editor.on('cursorActivity', cm => {
const { cursor, markdown } = this.getMarkdownAndCursor(cm)
const wordCount = getWordCount(markdown)
const line = cm.getLine(cursor.line)
const preLine = cm.getLine(cursor.line - 1)
const nextLine = cm.getLine(cursor.line + 1)
cursor = adjustCursor(cursor, preLine, line, nextLine)
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
this.$store.dispatch('LISTEN_FOR_CONTENT_CHANGE', { markdown, wordCount, cursor })
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)
})
},
setMarkdown (markdown) {
// A new file was opened or new tab was added.
setMarkdown ({ id, markdown }) {
this.prepareTabSwitch()
const { editor } = this
editor.setValue(markdown)
// // NOTE: Don't set the cursor because we load a new file - no tab switch.
// NOTE: Don't set the cursor because we load a new file.
setCursorAtLastLine(editor)
this.tabId = id
},
// Only listen to get changes. Do not set history or other things.
handleMarkdownChange({ markdown, cursor, renderCursor, history }) {
// Another tab was selected - only listen to get changes but don't set history or other things.
handleMarkdownChange ({ id, markdown, cursor, renderCursor, history }) {
this.prepareTabSwitch()
const { editor } = this
editor.setValue(markdown)
// Cursor is null when loading a file or creating a new tab in source code mode.
@ -130,6 +154,27 @@
} else {
setCursorAtLastLine(editor)
}
this.tabId = id
},
// Get markdown and cursor from CodeMirror.
getMarkdownAndCursor (cm) {
let cursor = cm.getCursor()
const markdown = cm.getValue()
const line = cm.getLine(cursor.line)
const preLine = cm.getLine(cursor.line - 1)
const nextLine = cm.getLine(cursor.line + 1)
cursor = adjustCursor(cursor, preLine, line, nextLine)
return { cursor, markdown }
},
// Commit changes from old tab. Problem: tab was already switched, so commit changes with old tab id.
prepareTabSwitch () {
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 })
this.tabId = null // invalidate tab id
}
}
}
}

View File

@ -11,10 +11,11 @@
@click.stop="selectFile(file)"
>
<span>{{ file.filename }}</span>
<svg class="icon" aria-hidden="true"
<svg class="close-icon icon" aria-hidden="true"
@click.stop="removeFileInTab(file)"
>
<use xlink:href="#icon-close-small"></use>
<circle id="unsaved-circle-icon" cx="6" cy="6" r="3"></circle>
<use id="default-close-icon" xlink:href="#icon-close-small"></use>
</svg>
</li>
<li class="new-file">
@ -49,6 +50,9 @@
</script>
<style scoped>
svg.close-icon #unsaved-circle-icon {
fill: var(--themeColor);
}
.editor-tabs {
width: 100%;
height: 35px;
@ -82,6 +86,12 @@
&:hover > svg {
opacity: 1;
}
&:hover > svg.close-icon #default-close-icon {
display: block !important;
}
&:hover > svg.close-icon #unsaved-circle-icon {
display: none !important;
}
& > span {
overflow: hidden;
text-overflow: ellipsis;
@ -89,6 +99,17 @@
margin-right: 3px;
}
}
& > li.unsaved:not(.active) {
& > svg.close-icon {
opacity: 1;
}
& > svg.close-icon #unsaved-circle-icon {
display: block;
}
& > svg.close-icon #default-close-icon {
display: none;
}
}
& > li.active {
background: var(--itemBgColor);
&:not(:last-child):after {
@ -103,6 +124,9 @@
& > svg {
opacity: 1;
}
& > svg.close-icon #unsaved-circle-icon {
display: none;
}
}
& > li.new-file {

View File

@ -25,11 +25,11 @@ const mutations = {
SET_CURRENT_FILE (state, currentFile) {
const oldCurrentFile = state.currentFile
if (!oldCurrentFile.id || oldCurrentFile.id !== currentFile.id) {
const { markdown, cursor, history, pathname } = currentFile
const { id, markdown, cursor, history, pathname } = currentFile
window.DIRNAME = pathname ? path.dirname(pathname) : ''
// set state first, then emit file changed event
state.currentFile = currentFile
bus.$emit('file-changed', { markdown, cursor, renderCursor: true, history })
bus.$emit('file-changed', { id, markdown, cursor, renderCursor: true, history })
}
},
ADD_FILE_TO_TABS (state, currentFile) {
@ -41,12 +41,12 @@ const mutations = {
tabs.splice(index, 1)
state.tabs = tabs
if (file.id === currentFile.id) {
const fileState = state.tabs[index] || state.tabs[index - 1] || {}
const fileState = state.tabs[index] || state.tabs[index - 1] || state.tabs[0] || {}
state.currentFile = fileState
if (typeof fileState.markdown === 'string') {
const { markdown, cursor, history, pathname } = fileState
const { id, markdown, cursor, history, pathname } = fileState
window.DIRNAME = pathname ? path.dirname(pathname) : ''
bus.$emit('file-changed', { markdown, cursor, renderCursor: true, history })
bus.$emit('file-changed', { id, markdown, cursor, renderCursor: true, history })
}
}
},
@ -55,7 +55,7 @@ const mutations = {
const { data, pathname } = change
const { isMixedLineEndings, lineEnding, adjustLineEndingOnSave, isUtf8BomEncoded, markdown, textDirection, filename } = data
const options = { isUtf8BomEncoded, lineEnding, adjustLineEndingOnSave, textDirection }
const fileState = getSingleFileState({ markdown, filename, pathname, options })
const newFileState = getSingleFileState({ markdown, filename, pathname, options })
if (isMixedLineEndings) {
notice.notify({
title: 'Line Ending',
@ -65,18 +65,26 @@ const mutations = {
showConfirm: false
})
}
let fileState = null
for (const tab of tabs) {
if (tab.pathname === pathname) {
Object.assign(tab, fileState)
const oldId = tab.id
Object.assign(tab, newFileState)
tab.id = oldId
fileState = tab
break
}
}
state.tabs = tabs
if (!fileState) {
throw new Error('LOAD_CHANGE: Cannot find tab in tab list.')
}
if (pathname === currentFile.pathname) {
Object.assign(currentFile, fileState)
state.currentFile = currentFile
const { cursor, history } = currentFile
bus.$emit('file-changed', { markdown, cursor, renderCursor: true, history })
state.currentFile = fileState
const { id, cursor, history } = fileState
bus.$emit('file-changed', { id, markdown, cursor, renderCursor: true, history })
}
},
SET_PATHNAME (state, file) {
@ -139,6 +147,7 @@ const mutations = {
}
},
CLOSE_TABS (state, arr) {
let tabIndex = 0
arr.forEach(id => {
const index = state.tabs.findIndex(f => f.id === id)
const { pathname } = state.tabs.find(f => f.id === id)
@ -151,14 +160,17 @@ const mutations = {
if (state.currentFile.id === id) {
state.currentFile = {}
window.DIRNAME = ''
if (arr.length === 1) {
tabIndex = index
}
}
})
if (!state.currentFile.id && state.tabs.length) {
state.currentFile = state.tabs[0]
state.currentFile = state.tabs[tabIndex] || state.tabs[tabIndex - 1] || state.tabs[0] || {}
if (typeof state.currentFile.markdown === 'string') {
const { markdown, cursor, history, pathname } = state.currentFile
const { id, markdown, cursor, history, pathname } = state.currentFile
window.DIRNAME = pathname ? path.dirname(pathname) : ''
bus.$emit('file-changed', { markdown, cursor, renderCursor: true, history })
bus.$emit('file-changed', { id, markdown, cursor, renderCursor: true, history })
}
}
},
@ -383,11 +395,12 @@ const actions = {
LISTEN_FOR_OPEN_SINGLE_FILE ({ commit, state, dispatch }) {
ipcRenderer.on('AGANI::open-single-file', (e, { markdown, filename, pathname, options }) => {
const fileState = getSingleFileState({ markdown, filename, pathname, options })
const { id } = fileState
const { lineEnding } = options
commit('SET_GLOBAL_LINE_ENDING', lineEnding)
dispatch('INIT_STATUS', true)
dispatch('UPDATE_CURRENT_FILE', fileState)
bus.$emit('file-loaded', markdown)
bus.$emit('file-loaded', { id, markdown })
commit('SET_LAYOUT', {
rightColumn: 'files',
showSideBar: false,
@ -424,12 +437,11 @@ const actions = {
NEW_BLANK_FILE ({ commit, state, dispatch }) {
dispatch('SHOW_TAB_VIEW', false)
const { tabs, lineEnding } = state
const fileState = getBlankFileState(tabs, lineEnding)
const { markdown } = fileState
const { id, markdown } = fileState
dispatch('UPDATE_CURRENT_FILE', fileState)
bus.$emit('file-loaded', markdown)
bus.$emit('file-loaded', { id, markdown })
},
/**
@ -449,8 +461,9 @@ const actions = {
const { markdown, isMixedLineEndings } = markdownDocument
const docState = createDocumentState(markdownDocument)
const { id } = docState
dispatch('UPDATE_CURRENT_FILE', docState)
bus.$emit('file-loaded', markdown)
bus.$emit('file-loaded', { id, markdown })
if (isMixedLineEndings) {
const { filename, lineEnding } = markdownDocument
@ -476,11 +489,11 @@ const actions = {
ipcRenderer.on('AGANI::open-blank-window', (e, { lineEnding, markdown: source }) => {
const { tabs } = state
const fileState = getBlankFileState(tabs, lineEnding, source)
const { markdown } = fileState
const { id, markdown } = fileState
commit('SET_GLOBAL_LINE_ENDING', lineEnding)
dispatch('INIT_STATUS', true)
dispatch('UPDATE_CURRENT_FILE', fileState)
bus.$emit('file-loaded', markdown)
bus.$emit('file-loaded', { id, markdown })
commit('SET_LAYOUT', {
rightColumn: 'files',
showSideBar: false,
@ -490,24 +503,34 @@ const actions = {
})
},
// LISTEN_FOR_FILE_CHANGE ({ commit, state }) {
// ipcRenderer.on('AGANI::file-change', (e, { file, filename, pathname }) => {
// const { windowActive } = state
// commit('SET_FILENAME', filename)
// commit('SET_PATHNAME', pathname)
// commit('SET_MARKDOWN', file)
// commit('SET_SAVE_STATUS', true)
// if (!windowActive) {
// bus.$emit('file-loaded', file)
// }
// })
// },
// Content change from realtime preview editor and source code editor
LISTEN_FOR_CONTENT_CHANGE ({ commit, state, rootState }, { markdown, wordCount, cursor, history, toc }) {
// WORKAROUND: id is "muya" if changes come from muya and not source code editor! So we don't have to apply the workaround.
LISTEN_FOR_CONTENT_CHANGE ({ commit, state, rootState }, { id, markdown, wordCount, cursor, history, toc }) {
const { autoSave } = rootState.preferences
const { projectTree } = rootState.project
const { pathname, markdown: oldMarkdown, id } = state.currentFile
const { id: currentId, pathname, markdown: oldMarkdown } = state.currentFile
if (!id) {
throw new Error(`Listen for document change but id was not set!`)
} else if (!currentId || state.tabs.length === 0) {
// Discard changes - this case should normally not occur.
return
} else if (id !== 'muya' && currentId !== id) {
// WORKAROUND: We commit changes after switching the tab in source code mode.
// Update old tab or discard changes
for (const tab of state.tabs) {
if (tab.id && tab.id === id) {
tab.markdown = markdown
// set cursor
if (cursor) tab.cursor = cursor
// set history
if (history) tab.history = history
break
}
}
return
}
const options = getOptionsFromState(state.currentFile)
commit('SET_MARKDOWN', markdown)
@ -531,7 +554,7 @@ const actions = {
commit('UPDATE_PROJECT_CONTENT', { markdown, pathname })
}
if (pathname && autoSave) {
ipcRenderer.send('AGANI::response-file-save', { id, pathname, markdown, options })
ipcRenderer.send('AGANI::response-file-save', { id: currentId, pathname, markdown, options })
} else {
commit('SET_SAVE_STATUS', false)
}