Skip to content

Commit

Permalink
Merge pull request #984 from savetheclocktower/markdown-preview-perfo…
Browse files Browse the repository at this point in the history
…rmance

[markdown-preview] Optimize re-rendering of content in a preview pane…
  • Loading branch information
savetheclocktower committed May 15, 2024
2 parents 5024f05 + 678c1b7 commit 217313b
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 149 deletions.
47 changes: 36 additions & 11 deletions packages/markdown-preview/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ const isMarkdownPreviewView = function (object) {
}

module.exports = {
activate () {
activate() {
this.disposables = new CompositeDisposable()
this.commandSubscriptions = new CompositeDisposable()

this.style = new CSSStyleSheet()

// TODO: When we upgrade Electron, we can push onto `adoptedStyleSheets`
// directly. For now, we have to do this silly thing.
let styleSheets = Array.from(document.adoptedStyleSheets ?? [])
styleSheets.push(this.style)
document.adoptedStyleSheets = styleSheets

this.disposables.add(
atom.config.observe('markdown-preview.grammars', grammars => {
this.commandSubscriptions.dispose()
Expand Down Expand Up @@ -53,6 +61,22 @@ module.exports = {
})
)

this.disposables.add(
atom.config.observe('editor.fontFamily', (fontFamily) => {
// Keep the user's `fontFamily` setting in sync with preview styles.
// `pre` blocks will use this font automatically, but `code` elements
// need a specific style rule.
//
// Since this applies to all content, we should declare this only once,
// instead of once per preview view.
this.style.replaceSync(`
.markdown-preview code {
font-family: ${fontFamily} !important;
}
`)
})
)

const previewFile = this.previewFile.bind(this)
for (const extension of [
'markdown',
Expand Down Expand Up @@ -94,12 +118,12 @@ module.exports = {
)
},

deactivate () {
deactivate() {
this.disposables.dispose()
this.commandSubscriptions.dispose()
},

createMarkdownPreviewView (state) {
createMarkdownPreviewView(state) {
if (state.editorId || fs.isFileSync(state.filePath)) {
if (MarkdownPreviewView == null) {
MarkdownPreviewView = require('./markdown-preview-view')
Expand All @@ -108,7 +132,7 @@ module.exports = {
}
},

toggle () {
toggle() {
if (isMarkdownPreviewView(atom.workspace.getActivePaneItem())) {
atom.workspace.destroyActivePaneItem()
return
Expand All @@ -129,11 +153,11 @@ module.exports = {
}
},

uriForEditor (editor) {
uriForEditor(editor) {
return `markdown-preview://editor/${editor.id}`
},

removePreviewForEditor (editor) {
removePreviewForEditor(editor) {
const uri = this.uriForEditor(editor)
const previewPane = atom.workspace.paneForURI(uri)
if (previewPane != null) {
Expand All @@ -144,7 +168,7 @@ module.exports = {
}
},

addPreviewForEditor (editor) {
addPreviewForEditor(editor) {
const uri = this.uriForEditor(editor)
const previousActivePane = atom.workspace.getActivePane()
const options = { searchAllPanes: true }
Expand All @@ -161,7 +185,7 @@ module.exports = {
})
},

previewFile ({ target }) {
previewFile({ target }) {
const filePath = target.dataset.path
if (!filePath) {
return
Expand All @@ -178,7 +202,7 @@ module.exports = {
})
},

async copyHTML () {
async copyHTML() {
const editor = atom.workspace.getActiveTextEditor()
if (editor == null) {
return
Expand All @@ -191,13 +215,14 @@ module.exports = {
const html = await renderer.toHTML(
text,
editor.getPath(),
editor.getGrammar()
editor.getGrammar(),
editor.id
)

atom.clipboard.write(html)
},

saveAsHTML () {
saveAsHTML() {
const activePaneItem = atom.workspace.getActivePaneItem()
if (isMarkdownPreviewView(activePaneItem)) {
atom.workspace.getActivePane().saveItemAs(activePaneItem)
Expand Down
58 changes: 45 additions & 13 deletions packages/markdown-preview/lib/markdown-preview-view.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const path = require('path')
const morphdom = require('morphdom')

const { Emitter, Disposable, CompositeDisposable, File } = require('atom')
const _ = require('underscore-plus')
Expand All @@ -17,6 +18,7 @@ module.exports = class MarkdownPreviewView {
this.element = document.createElement('div')
this.element.classList.add('markdown-preview')
this.element.tabIndex = -1

this.emitter = new Emitter()
this.loaded = false
this.disposables = new CompositeDisposable()
Expand All @@ -32,6 +34,7 @@ module.exports = class MarkdownPreviewView {
})
)
}
this.editorCache = new renderer.EditorCache(editorId)
}

serialize() {
Expand All @@ -52,6 +55,7 @@ module.exports = class MarkdownPreviewView {
destroy() {
this.disposables.dispose()
this.element.remove()
this.editorCache.destroy()
}

registerScrollCommands() {
Expand Down Expand Up @@ -83,7 +87,7 @@ module.exports = class MarkdownPreviewView {
return this.emitter.on('did-change-title', callback)
}

onDidChangeModified(callback) {
onDidChangeModified(_callback) {
// No op to suppress deprecation warning
return new Disposable()
}
Expand Down Expand Up @@ -270,7 +274,22 @@ module.exports = class MarkdownPreviewView {
return this.getMarkdownSource()
.then(source => {
if (source != null) {
return this.renderMarkdownText(source)
if (this.loaded) {
return this.renderMarkdownText(source);
} else {
// If we haven't loaded yet, defer before we render the Markdown
// for the first time. This allows the pane to appear and to
// display the loading indicator. Otherwise the first render
// happens before the pane is even visible.
//
// This doesn't slow anything down; it just shifts the work around
// so that the pane appears earlier in the cycle.
return new Promise((resolve) => {
setTimeout(() => {
resolve(this.renderMarkdownText(source))
}, 0)
})
}
}
})
.catch(reason => this.showError({ message: reason }))
Expand Down Expand Up @@ -309,18 +328,34 @@ module.exports = class MarkdownPreviewView {

async renderMarkdownText(text) {
const { scrollTop } = this.element

try {
const domFragment = await renderer.toDOMFragment(
const [domFragment, done] = await renderer.toDOMFragment(
text,
this.getPath(),
this.getGrammar()
this.getGrammar(),
this.editorId
)

this.loading = false
this.loaded = true
this.element.textContent = ''
this.element.appendChild(domFragment)

// Clone the existing container
let newElement = this.element.cloneNode(false)
newElement.appendChild(domFragment)

morphdom(this.element, newElement, {
onBeforeNodeDiscarded(node) {
// Don't discard `atom-text-editor` elements despite the fact that
// they don't exist in the new content.
if (node.nodeName === 'ATOM-TEXT-EDITOR') {
return false
}
}
})

await done(this.element)
this.element.classList.remove('loading')

this.emitter.emit('did-change-markdown')
this.element.scrollTop = scrollTop
} catch (error) {
Expand Down Expand Up @@ -400,7 +435,7 @@ module.exports = class MarkdownPreviewView {
.join('\n')
.replace(/atom-text-editor/g, 'pre.editor-colors')
.replace(/:host/g, '.host') // Remove shadow-dom :host selector causing problem on FF
.replace(cssUrlRegExp, function (match, assetsName, offset, string) {
.replace(cssUrlRegExp, function (_match, assetsName, _offset, _string) {
// base64 encode assets
const assetPath = path.join(__dirname, '../assets', assetsName)
const originalData = fs.readFileSync(assetPath, 'binary')
Expand All @@ -413,6 +448,7 @@ module.exports = class MarkdownPreviewView {

showError(result) {
this.element.textContent = ''
this.element.classList.remove('loading')
const h2 = document.createElement('h2')
h2.textContent = 'Previewing Markdown Failed'
this.element.appendChild(h2)
Expand All @@ -425,11 +461,7 @@ module.exports = class MarkdownPreviewView {

showLoading() {
this.loading = true
this.element.textContent = ''
const div = document.createElement('div')
div.classList.add('markdown-spinner')
div.textContent = 'Loading Markdown\u2026'
this.element.appendChild(div)
this.element.classList.add('loading')
}

selectAll() {
Expand Down

0 comments on commit 217313b

Please sign in to comment.