Skip to content

Commit 743c979

Browse files
authored
Merge pull request #2641 from daiyam/fix-paste-code
fix pasting into fenced code block
2 parents 3f77cb2 + d4dd74e commit 743c979

File tree

8 files changed

+210
-69
lines changed

8 files changed

+210
-69
lines changed

browser/components/CodeEditor.js

Lines changed: 134 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import crypto from 'crypto'
1313
import consts from 'browser/lib/consts'
1414
import styles from '../components/CodeEditor.styl'
1515
import fs from 'fs'
16-
const { ipcRenderer, remote } = require('electron')
16+
const { ipcRenderer, remote, clipboard } = require('electron')
1717
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
1818
const spellcheck = require('browser/lib/spellcheck')
1919
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
@@ -25,6 +25,10 @@ CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
2525
const buildCMRulers = (rulers, enableRulers) =>
2626
(enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
2727

28+
function translateHotkey (hotkey) {
29+
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
30+
}
31+
2832
export default class CodeEditor extends React.Component {
2933
constructor (props) {
3034
super(props)
@@ -56,7 +60,11 @@ export default class CodeEditor extends React.Component {
5660
noteKey
5761
)
5862
}
59-
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
63+
this.pasteHandler = (editor, e) => {
64+
e.preventDefault()
65+
66+
this.handlePaste(editor, false)
67+
}
6068
this.loadStyleHandler = e => {
6169
this.editor.refresh()
6270
}
@@ -124,42 +132,9 @@ export default class CodeEditor extends React.Component {
124132
}
125133
}
126134

127-
updateTableEditorState () {
128-
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
129-
if (active) {
130-
if (this.extraKeysMode !== 'editor') {
131-
this.extraKeysMode = 'editor'
132-
this.editor.setOption('extraKeys', this.editorKeyMap)
133-
}
134-
} else {
135-
if (this.extraKeysMode !== 'default') {
136-
this.extraKeysMode = 'default'
137-
this.editor.setOption('extraKeys', this.defaultKeyMap)
138-
this.tableEditor.resetSmartCursor()
139-
}
140-
}
141-
}
142-
143-
componentDidMount () {
144-
const { rulers, enableRulers, switchPreview } = this.props
135+
updateDefaultKeyMap () {
136+
const { hotkey } = this.props
145137
const expandSnippet = this.expandSnippet.bind(this)
146-
eventEmitter.on('line:jump', this.scrollToLineHandeler)
147-
148-
const defaultSnippet = [
149-
{
150-
id: crypto.randomBytes(16).toString('hex'),
151-
name: 'Dummy text',
152-
prefix: ['lorem', 'ipsum'],
153-
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
154-
}
155-
]
156-
if (!fs.existsSync(consts.SNIPPET_FILE)) {
157-
fs.writeFileSync(
158-
consts.SNIPPET_FILE,
159-
JSON.stringify(defaultSnippet, null, 4),
160-
'utf8'
161-
)
162-
}
163138

164139
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
165140
Tab: function (cm) {
@@ -211,8 +186,50 @@ export default class CodeEditor extends React.Component {
211186
document.execCommand('copy')
212187
}
213188
return CodeMirror.Pass
189+
},
190+
[translateHotkey(hotkey.pasteSmartly)]: cm => {
191+
this.handlePaste(cm, true)
214192
}
215193
})
194+
}
195+
196+
updateTableEditorState () {
197+
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
198+
if (active) {
199+
if (this.extraKeysMode !== 'editor') {
200+
this.extraKeysMode = 'editor'
201+
this.editor.setOption('extraKeys', this.editorKeyMap)
202+
}
203+
} else {
204+
if (this.extraKeysMode !== 'default') {
205+
this.extraKeysMode = 'default'
206+
this.editor.setOption('extraKeys', this.defaultKeyMap)
207+
this.tableEditor.resetSmartCursor()
208+
}
209+
}
210+
}
211+
212+
componentDidMount () {
213+
const { rulers, enableRulers } = this.props
214+
eventEmitter.on('line:jump', this.scrollToLineHandeler)
215+
216+
const defaultSnippet = [
217+
{
218+
id: crypto.randomBytes(16).toString('hex'),
219+
name: 'Dummy text',
220+
prefix: ['lorem', 'ipsum'],
221+
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
222+
}
223+
]
224+
if (!fs.existsSync(consts.SNIPPET_FILE)) {
225+
fs.writeFileSync(
226+
consts.SNIPPET_FILE,
227+
JSON.stringify(defaultSnippet, null, 4),
228+
'utf8'
229+
)
230+
}
231+
232+
this.updateDefaultKeyMap()
216233

217234
this.value = this.props.value
218235
this.editor = CodeMirror(this.refs.root, {
@@ -245,7 +262,7 @@ export default class CodeEditor extends React.Component {
245262
this.editor.on('blur', this.blurHandler)
246263
this.editor.on('change', this.changeHandler)
247264
this.editor.on('paste', this.pasteHandler)
248-
if (switchPreview !== 'RIGHTCLICK') {
265+
if (this.props.switchPreview !== 'RIGHTCLICK') {
249266
this.editor.on('contextmenu', this.contextMenuHandler)
250267
}
251268
eventEmitter.on('top:search', this.searchHandler)
@@ -479,6 +496,14 @@ export default class CodeEditor extends React.Component {
479496
this.editor.setOption('extraKeys', this.defaultKeyMap)
480497
}
481498

499+
if (prevProps.hotkey !== this.props.hotkey) {
500+
this.updateDefaultKeyMap()
501+
502+
if (this.extraKeysMode === 'default') {
503+
this.editor.setOption('extraKeys', this.defaultKeyMap)
504+
}
505+
}
506+
482507
if (this.state.clientWidth !== this.refs.root.clientWidth) {
483508
this.setState({
484509
clientWidth: this.refs.root.clientWidth
@@ -567,15 +592,14 @@ export default class CodeEditor extends React.Component {
567592
this.editor.replaceSelection(imageMd)
568593
}
569594

570-
handlePaste (editor, e) {
571-
const clipboardData = e.clipboardData
572-
const { storageKey, noteKey } = this.props
573-
const dataTransferItem = clipboardData.items[0]
574-
const pastedTxt = clipboardData.getData('text')
595+
handlePaste (editor, forceSmartPaste) {
596+
const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
597+
575598
const isURL = str => {
576599
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
577600
return matcher.test(str)
578601
}
602+
579603
const isInLinkTag = editor => {
580604
const startCursor = editor.getCursor('start')
581605
const prevChar = editor.getRange(
@@ -590,30 +614,73 @@ export default class CodeEditor extends React.Component {
590614
return prevChar === '](' && nextChar === ')'
591615
}
592616

593-
const pastedHtml = clipboardData.getData('text/html')
594-
if (pastedHtml !== '') {
595-
this.handlePasteHtml(e, editor, pastedHtml)
596-
} else if (dataTransferItem.type.match('image')) {
597-
attachmentManagement.handlePastImageEvent(
598-
this,
599-
storageKey,
600-
noteKey,
601-
dataTransferItem
602-
)
603-
} else if (
604-
this.props.fetchUrlTitle &&
605-
isURL(pastedTxt) &&
606-
!isInLinkTag(editor)
607-
) {
608-
this.handlePasteUrl(e, editor, pastedTxt)
617+
const isInFencedCodeBlock = editor => {
618+
const cursor = editor.getCursor()
619+
620+
let token = editor.getTokenAt(cursor)
621+
if (token.state.fencedState) {
622+
return true
623+
}
624+
625+
let line = line = cursor.line - 1
626+
while (line >= 0) {
627+
token = editor.getTokenAt({
628+
ch: 3,
629+
line
630+
})
631+
632+
if (token.start === token.end) {
633+
--line
634+
} else if (token.type === 'comment') {
635+
if (line > 0) {
636+
token = editor.getTokenAt({
637+
ch: 3,
638+
line: line - 1
639+
})
640+
641+
return token.type !== 'comment'
642+
} else {
643+
return true
644+
}
645+
} else {
646+
return false
647+
}
648+
}
649+
650+
return false
609651
}
610-
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
652+
653+
const pastedTxt = clipboard.readText()
654+
655+
if (isInFencedCodeBlock(editor)) {
656+
this.handlePasteText(editor, pastedTxt)
657+
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
658+
this.handlePasteUrl(editor, pastedTxt)
659+
} else if (enableSmartPaste || forceSmartPaste) {
660+
const image = clipboard.readImage()
661+
if (!image.isEmpty()) {
662+
attachmentManagement.handlePastNativeImage(
663+
this,
664+
storageKey,
665+
noteKey,
666+
image
667+
)
668+
} else {
669+
const pastedHtml = clipboard.readHTML()
670+
if (pastedHtml.length > 0) {
671+
this.handlePasteHtml(editor, pastedHtml)
672+
} else {
673+
this.handlePasteText(editor, pastedTxt)
674+
}
675+
}
676+
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
611677
attachmentManagement
612678
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
613679
.then(modifiedText => {
614680
this.editor.replaceSelection(modifiedText)
615681
})
616-
e.preventDefault()
682+
} else {
683+
this.handlePasteText(editor, pastedTxt)
617684
}
618685
}
619686

@@ -623,8 +690,7 @@ export default class CodeEditor extends React.Component {
623690
}
624691
}
625692

626-
handlePasteUrl (e, editor, pastedTxt) {
627-
e.preventDefault()
693+
handlePasteUrl (editor, pastedTxt) {
628694
const taggedUrl = `<${pastedTxt}>`
629695
editor.replaceSelection(taggedUrl)
630696

@@ -663,12 +729,15 @@ export default class CodeEditor extends React.Component {
663729
})
664730
}
665731

666-
handlePasteHtml (e, editor, pastedHtml) {
667-
e.preventDefault()
732+
handlePasteHtml (editor, pastedHtml) {
668733
const markdown = this.turndownService.turndown(pastedHtml)
669734
editor.replaceSelection(markdown)
670735
}
671736

737+
handlePasteText (editor, pastedTxt) {
738+
editor.replaceSelection(pastedTxt)
739+
}
740+
672741
mapNormalResponse (response, pastedTxt) {
673742
return this.decodeResponse(response).then(body => {
674743
return new Promise((resolve, reject) => {

browser/components/MarkdownEditor.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ class MarkdownEditor extends React.Component {
278278
onChange={(e) => this.handleChange(e)}
279279
onBlur={(e) => this.handleBlur(e)}
280280
spellCheck={config.editor.spellcheck}
281+
enableSmartPaste={config.editor.enableSmartPaste}
282+
hotkey={config.hotkey}
281283
switchPreview={config.editor.switchPreview}
282284
/>
283285
<MarkdownPreview styleName={this.state.status === 'PREVIEW'

browser/components/MarkdownSplitEditor.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ class MarkdownSplitEditor extends React.Component {
172172
onChange={this.handleOnChange.bind(this)}
173173
onScroll={this.handleScroll.bind(this)}
174174
spellCheck={config.editor.spellcheck}
175+
enableSmartPaste={config.editor.enableSmartPaste}
176+
hotkey={config.hotkey}
175177
switchPreview={config.editor.switchPreview}
176178
/>
177179
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >

browser/main/Detail/SnippetNoteDetail.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,8 @@ class SnippetNoteDetail extends React.Component {
724724
enableTableEditor={config.editor.enableTableEditor}
725725
onChange={(e) => this.handleCodeChange(index)(e)}
726726
ref={'code-' + index}
727+
enableSmartPaste={config.editor.enableSmartPaste}
728+
hotkey={config.hotkey}
727729
/>
728730
}
729731
</div>

browser/main/lib/ConfigManager.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export const DEFAULT_CONFIG = {
2525
hotkey: {
2626
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
2727
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
28-
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
28+
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
29+
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V'
2930
},
3031
ui: {
3132
language: 'en',
@@ -52,7 +53,8 @@ export const DEFAULT_CONFIG = {
5253
enableTableEditor: false,
5354
enableFrontMatterTitle: true,
5455
frontMatterTitleField: 'title',
55-
spellcheck: false
56+
spellcheck: false,
57+
enableSmartPaste: false
5658
},
5759
preview: {
5860
fontSize: '14',

browser/main/lib/dataApi/attachmentManagement.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
316316
reader.readAsDataURL(blob)
317317
}
318318

319+
/**
320+
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
321+
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
322+
* @param {String} storageKey Key of the current storage
323+
* @param {String} noteKey Key of the current note
324+
* @param {NativeImage} image The native image
325+
*/
326+
function handlePastNativeImage (codeEditor, storageKey, noteKey, image) {
327+
if (!codeEditor) {
328+
throw new Error('codeEditor has to be given')
329+
}
330+
if (!storageKey) {
331+
throw new Error('storageKey has to be given')
332+
}
333+
334+
if (!noteKey) {
335+
throw new Error('noteKey has to be given')
336+
}
337+
if (!image) {
338+
throw new Error('image has to be given')
339+
}
340+
341+
const targetStorage = findStorage.findStorage(storageKey)
342+
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
343+
344+
createAttachmentDestinationFolder(targetStorage.path, noteKey)
345+
346+
const imageName = `${uniqueSlug()}.png`
347+
const imagePath = path.join(destinationDir, imageName)
348+
349+
const binaryData = image.toPNG()
350+
fs.writeFileSync(imagePath, binaryData, 'binary')
351+
352+
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
353+
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
354+
codeEditor.insertAttachmentMd(imageMd)
355+
}
356+
319357
/**
320358
* @description Returns all attachment paths of the given markdown
321359
* @param {String} markdownContent content in which the attachment paths should be found
@@ -539,6 +577,7 @@ module.exports = {
539577
generateAttachmentMarkdown,
540578
handleAttachmentDrop,
541579
handlePastImageEvent,
580+
handlePastNativeImage,
542581
getAttachmentsInMarkdownContent,
543582
getAbsolutePathsOfAttachmentsInContent,
544583
removeStorageAndNoteReferences,

0 commit comments

Comments
 (0)