-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Text expansion support #1848
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Text expansion support #1848
Changes from 20 commits
2e9b478
50d2f90
d3b3e45
ff2e399
8925f7c
358458a
ddcd722
a7b85b1
a7d0a4b
5e7bdf7
e88694b
78957cf
291d766
a2592e4
2e09501
8c43f3d
106f5a5
2bc0bce
f5a9d39
ce594b0
2b2f175
713615e
680c2a2
c2c5081
10500c3
172ea82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,8 +6,10 @@ import 'codemirror-mode-elixir' | |
| import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' | ||
| import eventEmitter from 'browser/main/lib/eventEmitter' | ||
| import iconv from 'iconv-lite' | ||
|
|
||
| const { ipcRenderer } = require('electron') | ||
| import crypto from 'crypto' | ||
| import consts from 'browser/lib/consts' | ||
| import fs from 'fs' | ||
| const { ipcRenderer, remote } = require('electron') | ||
|
|
||
| CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' | ||
|
|
||
|
|
@@ -92,8 +94,21 @@ export default class CodeEditor extends React.Component { | |
|
|
||
| componentDidMount () { | ||
| const { rulers, enableRulers } = this.props | ||
| this.value = this.props.value | ||
| const expandSnippet = this.expandSnippet.bind(this) | ||
|
|
||
| const defaultSnippet = [ | ||
| { | ||
| id: crypto.randomBytes(16).toString('hex'), | ||
| name: 'Dummy text', | ||
| prefix: ['lorem', 'ipsum'], | ||
| 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.' | ||
| } | ||
| ] | ||
| if (!fs.existsSync(consts.SNIPPET_FILE)) { | ||
| fs.writeFileSync(consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8') | ||
| } | ||
|
|
||
| this.value = this.props.value | ||
| this.editor = CodeMirror(this.refs.root, { | ||
| rulers: buildCMRulers(rulers, enableRulers), | ||
| value: this.props.value, | ||
|
|
@@ -114,6 +129,8 @@ export default class CodeEditor extends React.Component { | |
| Tab: function (cm) { | ||
| const cursor = cm.getCursor() | ||
| const line = cm.getLine(cursor.line) | ||
| const cursorPosition = cursor.ch | ||
| const charBeforeCursor = line.substr(cursorPosition - 1, 1) | ||
| if (cm.somethingSelected()) cm.indentSelection('add') | ||
| else { | ||
| const tabs = cm.getOption('indentWithTabs') | ||
|
|
@@ -125,6 +142,16 @@ export default class CodeEditor extends React.Component { | |
| cm.execCommand('insertSoftTab') | ||
| } | ||
| cm.execCommand('goLineEnd') | ||
| } else if (!charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) { | ||
| // text expansion on tab key if the char before is alphabet | ||
| const snippets = JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8')) | ||
| if (expandSnippet(line, cursor, cm, snippets) === false) { | ||
| if (tabs) { | ||
| cm.execCommand('insertTab') | ||
| } else { | ||
| cm.execCommand('insertSoftTab') | ||
| } | ||
| } | ||
| } else { | ||
| if (tabs) { | ||
| cm.execCommand('insertTab') | ||
|
|
@@ -168,6 +195,73 @@ export default class CodeEditor extends React.Component { | |
| CodeMirror.Vim.map('ZZ', ':q', 'normal') | ||
| } | ||
|
|
||
| expandSnippet (line, cursor, cm, snippets) { | ||
| const wordBeforeCursor = this.getWordBeforeCursor(line, cursor.line, cursor.ch) | ||
| const templateCursorString = ':{}' | ||
| for (let i = 0; i < snippets.length; i++) { | ||
| if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) { | ||
| if (snippets[i].content.indexOf(templateCursorString) !== -1) { | ||
| let snippetLines = snippets[i].content.split('\n') | ||
|
||
| let cursorLineNumber = 0 | ||
| let cursorLinePosition = 0 | ||
| for (let j = 0; j < snippetLines.length; j++) { | ||
| let cursorIndex = snippetLines[j].indexOf(templateCursorString) | ||
|
||
| if (cursorIndex !== -1) { | ||
| cursorLineNumber = j | ||
| cursorLinePosition = cursorIndex | ||
| cm.replaceRange( | ||
| snippets[i].content.replace(templateCursorString, ''), | ||
| wordBeforeCursor.range.from, | ||
| wordBeforeCursor.range.to | ||
| ) | ||
| cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition }) | ||
| } | ||
| } | ||
| } else { | ||
| cm.replaceRange( | ||
| snippets[i].content, | ||
| wordBeforeCursor.range.from, | ||
| wordBeforeCursor.range.to | ||
| ) | ||
| } | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| getWordBeforeCursor (line, lineNumber, cursorPosition) { | ||
| let wordBeforeCursor = '' | ||
| const originCursorPosition = cursorPosition | ||
| const emptyChars = /\t|\s|\r|\n/ | ||
|
|
||
| // to prevent the word to expand is long that will crash the whole app | ||
| // the safeStop is there to stop user to expand words that longer than 20 chars | ||
| const safeStop = 20 | ||
|
|
||
| while (cursorPosition > 0) { | ||
| const currentChar = line.substr(cursorPosition - 1, 1) | ||
| // if char is not an empty char | ||
| if (!emptyChars.test(currentChar)) { | ||
| wordBeforeCursor = currentChar + wordBeforeCursor | ||
| } else if (wordBeforeCursor.length >= safeStop) { | ||
| throw new Error('Your snippet trigger is too long !') | ||
| } else { | ||
| break | ||
| } | ||
| cursorPosition-- | ||
| } | ||
|
|
||
| return { | ||
| text: wordBeforeCursor, | ||
| range: { | ||
| from: {line: lineNumber, ch: originCursorPosition}, | ||
| to: {line: lineNumber, ch: cursorPosition} | ||
| } | ||
| } | ||
| } | ||
|
|
||
| quitEditor () { | ||
| document.querySelector('textarea').blur() | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ const path = require('path') | |
| const fs = require('sander') | ||
| const { remote } = require('electron') | ||
| const { app } = remote | ||
| const os = require('os') | ||
|
||
|
|
||
| const themePath = process.env.NODE_ENV === 'production' | ||
| ? path.join(app.getAppPath(), './node_modules/codemirror/theme') | ||
|
|
@@ -12,6 +13,10 @@ const themes = fs.readdirSync(themePath) | |
| }) | ||
| themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light') | ||
|
|
||
| const snippetFile = process.env.NODE_ENV !== 'test' | ||
| ? path.join(app.getPath('appData'), 'Boostnote', 'snippets.json') | ||
| : '' // return nothing as we specified different path to snippets.json in test | ||
|
|
||
| const consts = { | ||
| FOLDER_COLORS: [ | ||
| '#E10051', | ||
|
|
@@ -31,7 +36,8 @@ const consts = { | |
| 'Dodger Blue', | ||
| 'Violet Eggplant' | ||
| ], | ||
| THEMES: ['default'].concat(themes) | ||
| THEMES: ['default'].concat(themes), | ||
| SNIPPET_FILE: snippetFile | ||
| } | ||
|
|
||
| module.exports = consts | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import fs from 'fs' | ||
| import crypto from 'crypto' | ||
| import consts from 'browser/lib/consts' | ||
| import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet' | ||
|
|
||
| function createSnippet (snippetFile) { | ||
| return new Promise((resolve, reject) => { | ||
| const newSnippet = { | ||
| id: crypto.randomBytes(16).toString('hex'), | ||
| name: 'Unnamed snippet', | ||
| prefix: [], | ||
| content: '' | ||
| } | ||
| fetchSnippet(null, snippetFile).then((snippets) => { | ||
| snippets.push(newSnippet) | ||
| fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => { | ||
| if (err) reject(err) | ||
| resolve(newSnippet) | ||
| }) | ||
| }).catch((err) => { | ||
| reject(err) | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| module.exports = createSnippet |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import fs from 'fs' | ||
| import consts from 'browser/lib/consts' | ||
| import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet' | ||
|
|
||
| function deleteSnippet (snippet, snippetFile) { | ||
| return new Promise((resolve, reject) => { | ||
| fetchSnippet(null, snippetFile).then((snippets) => { | ||
| snippets = snippets.filter(currentSnippet => currentSnippet.id !== snippet.id) | ||
| fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => { | ||
| if (err) reject(err) | ||
| resolve(snippet) | ||
| }) | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| module.exports = deleteSnippet |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import fs from 'fs' | ||
| import crypto from 'crypto' | ||
| import consts from 'browser/lib/consts' | ||
|
|
||
| function fetchSnippet (id, snippetFile) { | ||
| return new Promise((resolve, reject) => { | ||
| fs.readFile(snippetFile || consts.SNIPPET_FILE, 'utf8', (err, data) => { | ||
| if (err) { | ||
| reject(err) | ||
| } | ||
| const snippets = JSON.parse(data) | ||
| if (id) { | ||
| const snippet = snippets.find(snippet => { return snippet.id === id }) | ||
| resolve(snippet) | ||
| } | ||
| resolve(snippets) | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| module.exports = fetchSnippet |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import fs from 'fs' | ||
| import consts from 'browser/lib/consts' | ||
|
|
||
| function updateSnippet (snippet, snippetFile) { | ||
| return new Promise((resolve, reject) => { | ||
| const snippets = JSON.parse(fs.readFileSync(snippetFile || consts.SNIPPET_FILE, 'utf-8')) | ||
|
|
||
| for (let i = 0; i < snippets.length; i++) { | ||
| const currentSnippet = snippets[i] | ||
|
|
||
| if (currentSnippet.id === snippet.id) { | ||
| if ( | ||
| currentSnippet.name === snippet.name && | ||
| currentSnippet.prefix === snippet.prefix && | ||
| currentSnippet.content === snippet.content | ||
| ) { | ||
| // if everything is the same then don't write to disk | ||
| resolve(snippets) | ||
| } else { | ||
| currentSnippet.name = snippet.name | ||
| currentSnippet.prefix = snippet.prefix | ||
| currentSnippet.content = snippet.content | ||
| fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => { | ||
| if (err) reject(err) | ||
| resolve(snippets) | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| module.exports = updateSnippet |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import CodeMirror from 'codemirror' | ||
| import React from 'react' | ||
| import _ from 'lodash' | ||
| import fs from 'fs' | ||
|
||
| import consts from 'browser/lib/consts' | ||
|
||
| import dataApi from 'browser/main/lib/dataApi' | ||
|
|
||
| const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] | ||
| const buildCMRulers = (rulers, enableRulers) => | ||
| enableRulers ? rulers.map(ruler => ({ column: ruler })) : [] | ||
|
|
||
| export default class SnippetEditor extends React.Component { | ||
|
|
||
| componentDidMount () { | ||
| this.props.onRef(this) | ||
| const { rulers, enableRulers } = this.props | ||
| this.cm = CodeMirror(this.refs.root, { | ||
| rulers: buildCMRulers(rulers, enableRulers), | ||
| lineNumbers: this.props.displayLineNumbers, | ||
| lineWrapping: true, | ||
| theme: this.props.theme, | ||
| indentUnit: this.props.indentSize, | ||
| tabSize: this.props.indentSize, | ||
| indentWithTabs: this.props.indentType !== 'space', | ||
| keyMap: this.props.keyMap, | ||
| scrollPastEnd: this.props.scrollPastEnd, | ||
| dragDrop: false, | ||
| foldGutter: true, | ||
| gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], | ||
| autoCloseBrackets: true, | ||
| mode: 'null' | ||
| }) | ||
| this.cm.setSize('100%', '100%') | ||
| let changeDelay = null | ||
|
|
||
| this.cm.on('change', () => { | ||
| this.snippet.content = this.cm.getValue() | ||
|
|
||
| clearTimeout(changeDelay) | ||
| changeDelay = setTimeout(() => { | ||
| this.saveSnippet() | ||
| }, 500) | ||
| }) | ||
| } | ||
|
|
||
| componentWillUnmount () { | ||
| this.props.onRef(undefined) | ||
| } | ||
|
|
||
| onSnippetChanged (newSnippet) { | ||
| this.snippet = newSnippet | ||
| this.cm.setValue(this.snippet.content) | ||
| } | ||
|
|
||
| onSnippetNameOrPrefixChanged (newSnippet) { | ||
| this.snippet.name = newSnippet.name | ||
| this.snippet.prefix = newSnippet.prefix.toString().replace(/\s/g, '').split(',') | ||
| this.saveSnippet() | ||
| } | ||
|
|
||
| saveSnippet () { | ||
| dataApi.updateSnippet(this.snippet).catch((err) => { throw err }) | ||
| } | ||
|
|
||
| render () { | ||
| const { fontSize } = this.props | ||
| let fontFamily = this.props.fontFamily | ||
| fontFamily = _.isString(fontFamily) && fontFamily.length > 0 | ||
| ? [fontFamily].concat(defaultEditorFontFamily) | ||
| : defaultEditorFontFamily | ||
| return ( | ||
| <div styleName='SnippetEditor' ref='root' tabIndex='-1' style={{ | ||
| fontFamily: fontFamily.join(', '), | ||
| fontSize: fontSize, | ||
| position: 'absolute', | ||
|
||
| width: '100%', | ||
| height: '90%' | ||
| }} /> | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| SnippetEditor.defaultProps = { | ||
| readOnly: false, | ||
| theme: 'xcode', | ||
| keyMap: 'sublime', | ||
| fontSize: 14, | ||
| fontFamily: 'Monaco, Consolas', | ||
| indentSize: 4, | ||
| indentType: 'space' | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remoteis never used, please remove it.