diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index d81ce39d..7f6b4bbd 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -5,7 +5,7 @@ import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import convertModeName from 'browser/lib/convertModeName' -import { options, TableEditor } from '@susisu/mte-kernel' +import { options, TableEditor, Alignment } from '@susisu/mte-kernel' import TextEditorInterface from 'browser/lib/TextEditorInterface' import eventEmitter from 'browser/main/lib/eventEmitter' import iconv from 'iconv-lite' @@ -59,6 +59,7 @@ export default class CodeEditor extends React.Component { this.searchState = null this.formatTable = () => this.handleFormatTable() + this.editorActivityHandler = () => this.handleEditorActivity() } handleSearch (msg) { @@ -99,6 +100,28 @@ export default class CodeEditor extends React.Component { this.tableEditor.formatAll(options({textWidthOptions: {}})) } + handleEditorActivity () { + if (!this.textEditorInterface.transaction) { + this.updateTableEditorState() + } + } + + updateTableEditorState () { + const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) + if (active) { + if (this.extraKeysMode !== 'editor') { + this.extraKeysMode = 'editor' + this.editor.setOption('extraKeys', this.editorKeyMap) + } + } else { + if (this.extraKeysMode !== 'default') { + this.extraKeysMode = 'default' + this.editor.setOption('extraKeys', this.defaultKeyMap) + this.tableEditor.resetSmartCursor() + } + } + } + componentDidMount () { const { rulers, enableRulers } = this.props const expandSnippet = this.expandSnippet.bind(this) @@ -119,6 +142,59 @@ export default class CodeEditor extends React.Component { ) } + this.defaultKeyMap = CodeMirror.normalizeKeyMap({ + 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') + if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) { + cm.execCommand('goLineStart') + if (tabs) { + cm.execCommand('insertTab') + } else { + 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') + } else { + cm.execCommand('insertSoftTab') + } + } + } + }, + 'Cmd-T': function (cm) { + // Do nothing + }, + Enter: 'boostNewLineAndIndentContinueMarkdownList', + 'Ctrl-C': cm => { + if (cm.getOption('keyMap').substr(0, 3) === 'vim') { + document.execCommand('copy') + } + return CodeMirror.Pass + } + }) + this.value = this.props.value this.editor = CodeMirror(this.refs.root, { rulers: buildCMRulers(rulers, enableRulers), @@ -141,58 +217,7 @@ export default class CodeEditor extends React.Component { explode: '[]{}``$$', override: true }, - extraKeys: { - 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') - if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) { - cm.execCommand('goLineStart') - if (tabs) { - cm.execCommand('insertTab') - } else { - 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') - } else { - cm.execCommand('insertSoftTab') - } - } - } - }, - 'Cmd-T': function (cm) { - // Do nothing - }, - Enter: 'boostNewLineAndIndentContinueMarkdownList', - 'Ctrl-C': cm => { - if (cm.getOption('keyMap').substr(0, 3) === 'vim') { - document.execCommand('copy') - } - return CodeMirror.Pass - } - } + extraKeys: this.defaultKeyMap }) this.setMode(this.props.mode) @@ -215,8 +240,58 @@ export default class CodeEditor extends React.Component { CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor) CodeMirror.Vim.map('ZZ', ':q', 'normal') - this.tableEditor = new TableEditor(new TextEditorInterface(this.editor)) + this.textEditorInterface = new TextEditorInterface(this.editor) + this.tableEditor = new TableEditor(this.textEditorInterface) eventEmitter.on('code:format-table', this.formatTable) + + this.tableEditorOptions = options({ + smartCursor: true + }) + + this.editorKeyMap = CodeMirror.normalizeKeyMap({ + 'Tab': () => { this.tableEditor.nextCell(this.tableEditorOptions) }, + 'Shift-Tab': () => { this.tableEditor.previousCell(this.tableEditorOptions) }, + 'Enter': () => { this.tableEditor.nextRow(this.tableEditorOptions) }, + 'Ctrl-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) }, + 'Cmd-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) }, + 'Shift-Ctrl-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) }, + 'Shift-Cmd-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) }, + 'Shift-Ctrl-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) }, + 'Shift-Cmd-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) }, + 'Shift-Ctrl-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) }, + 'Shift-Cmd-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) }, + 'Shift-Ctrl-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) }, + 'Shift-Cmd-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) }, + 'Ctrl-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) }, + 'Cmd-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) }, + 'Ctrl-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) }, + 'Cmd-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) }, + 'Ctrl-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) }, + 'Cmd-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) }, + 'Ctrl-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) }, + 'Cmd-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) }, + 'Ctrl-K Ctrl-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) }, + 'Cmd-K Cmd-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) }, + 'Ctrl-L Ctrl-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) }, + 'Cmd-L Cmd-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) }, + 'Ctrl-K Ctrl-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) }, + 'Cmd-K Cmd-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) }, + 'Ctrl-L Ctrl-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) }, + 'Cmd-L Cmd-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) }, + 'Alt-Shift-Ctrl-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) }, + 'Alt-Shift-Cmd-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) }, + 'Alt-Shift-Ctrl-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) }, + 'Alt-Shift-Cmd-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) }, + 'Alt-Shift-Ctrl-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) }, + 'Alt-Shift-Cmd-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) }, + 'Alt-Shift-Ctrl-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) }, + 'Alt-Shift-Cmd-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) } + }) + + if (this.props.enableTableEditor) { + this.editor.on('cursorActivity', this.editorActivityHandler) + this.editor.on('changes', this.editorActivityHandler) + } } expandSnippet (line, cursor, cm, snippets) { @@ -353,6 +428,19 @@ export default class CodeEditor extends React.Component { this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) } + if (prevProps.enableTableEditor !== this.props.enableTableEditor) { + if (this.props.enableTableEditor) { + this.editor.on('cursorActivity', this.editorActivityHandler) + this.editor.on('changes', this.editorActivityHandler) + } else { + this.editor.off('cursorActivity', this.editorActivityHandler) + this.editor.off('changes', this.editorActivityHandler) + } + + this.extraKeysMode = 'default' + this.editor.setOption('extraKeys', this.defaultKeyMap) + } + if (needRefresh) { this.editor.refresh() } diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index ee80c887..4c195797 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -265,6 +265,7 @@ class MarkdownEditor extends React.Component { storageKey={storageKey} noteKey={noteKey} fetchUrlTitle={config.editor.fetchUrlTitle} + enableTableEditor={config.editor.enableTableEditor} onChange={(e) => this.handleChange(e)} onBlur={(e) => this.handleBlur(e)} /> diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 8fa3cc07..ddc9d7e0 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -158,6 +158,7 @@ class MarkdownSplitEditor extends React.Component { rulers={config.editor.rulers} scrollPastEnd={config.editor.scrollPastEnd} fetchUrlTitle={config.editor.fetchUrlTitle} + enableTableEditor={config.editor.enableTableEditor} storageKey={storageKey} noteKey={noteKey} onChange={this.handleOnChange.bind(this)} diff --git a/browser/lib/TextEditorInterface.js b/browser/lib/TextEditorInterface.js index 53ae2337..55f2c29d 100644 --- a/browser/lib/TextEditorInterface.js +++ b/browser/lib/TextEditorInterface.js @@ -1,53 +1,113 @@ -import { Point } from '@susisu/mte-kernel' - -export default class TextEditorInterface { - constructor (editor) { - this.editor = editor - } - - getCursorPosition () { - const pos = this.editor.getCursor() - return new Point(pos.line, pos.ch) - } - - setCursorPosition (pos) { - this.editor.setCursor({line: pos.row, ch: pos.column}) - } - - setSelectionRange (range) { - this.editor.setSelection({ - anchor: {line: range.start.row, ch: range.start.column}, - head: {line: range.end.row, ch: range.end.column} - }) - } - - getLastRow () { - return this.editor.lastLine() - } - - acceptsTableEdit (row) { - return true - } - - getLine (row) { - return this.editor.getLine(row) - } - - insertLine (row, line) { - this.editor.replaceRange(line, {line: row, ch: 0}) - } - - deleteLine (row) { - this.editor.replaceRange('', {line: row, ch: 0}, {line: row, ch: this.editor.getLine(row).length}) - } - - replaceLines (startRow, endRow, lines) { - endRow-- // because endRow is a first line after a table. - const endRowCh = this.editor.getLine(endRow).length - this.editor.replaceRange(lines, {line: startRow, ch: 0}, {line: endRow, ch: endRowCh}) - } - - transact (func) { - func() - } -} +import { Point } from '@susisu/mte-kernel' + +export default class TextEditorInterface { + constructor (editor) { + this.editor = editor + this.doc = editor.getDoc() + this.transaction = false + } + + getCursorPosition () { + const { line, ch } = this.doc.getCursor() + return new Point(line, ch) + } + + setCursorPosition (pos) { + this.doc.setCursor({ + line: pos.row, + ch: pos.column + }) + } + + setSelectionRange (range) { + this.doc.setSelection( + { line: range.start.row, ch: range.start.column }, + { line: range.end.row, ch: range.end.column } + ) + } + + getLastRow () { + return this.doc.lineCount() - 1 + } + + acceptsTableEdit () { + return true + } + + getLine (row) { + return this.doc.getLine(row) + } + + insertLine (row, line) { + const lastRow = this.getLastRow() + if (row > lastRow) { + const lastLine = this.getLine(lastRow) + this.doc.replaceRange( + '\n' + line, + { line: lastRow, ch: lastLine.length }, + { line: lastRow, ch: lastLine.length } + ) + } else { + this.doc.replaceRange( + line + '\n', + { line: row, ch: 0 }, + { line: row, ch: 0 } + ) + } + } + + deleteLine (row) { + const lastRow = this.getLastRow() + if (row >= lastRow) { + if (lastRow > 0) { + const preLastLine = this.getLine(lastRow - 1) + const lastLine = this.getLine(lastRow) + this.doc.replaceRange( + '', + { line: lastRow - 1, ch: preLastLine.length }, + { line: lastRow, ch: lastLine.length } + ) + } else { + const lastLine = this.getLine(lastRow) + this.doc.replaceRange( + '', + { line: lastRow, ch: 0 }, + { line: lastRow, ch: lastLine.length } + ) + } + } else { + this.doc.replaceRange( + '', + { line: row, ch: 0 }, + { line: row + 1, ch: 0 } + ) + } + } + + replaceLines (startRow, endRow, lines) { + const lastRow = this.getLastRow() + if (endRow > lastRow) { + const lastLine = this.getLine(lastRow) + this.doc.replaceRange( + lines.join('\n'), + { line: startRow, ch: 0 }, + { line: lastRow, ch: lastLine.length } + ) + } else { + this.doc.replaceRange( + lines.join('\n') + '\n', + { line: startRow, ch: 0 }, + { line: endRow, ch: 0 } + ) + } + } + + transact (func) { + this.transaction = true + func() + this.transaction = false + if (this.onDidFinishTransaction) { + this.onDidFinishTransaction.call(undefined) + } + } +} diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 7229dce1..39d66eba 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -692,6 +692,7 @@ class SnippetNoteDetail extends React.Component { keyMap={config.editor.keyMap} scrollPastEnd={config.editor.scrollPastEnd} fetchUrlTitle={config.editor.fetchUrlTitle} + enableTableEditor={config.editor.enableTableEditor} onChange={(e) => this.handleCodeChange(index)(e)} ref={'code-' + index} /> diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 434b0d22..2c601b57 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -46,7 +46,8 @@ export const DEFAULT_CONFIG = { switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR scrollPastEnd: false, type: 'SPLIT', - fetchUrlTitle: true + fetchUrlTitle: true, + enableTableEditor: false }, preview: { fontSize: '14', diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index f68d6815..2d564042 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -90,7 +90,8 @@ class UiTab extends React.Component { keyMap: this.refs.editorKeyMap.value, snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value, scrollPastEnd: this.refs.scrollPastEnd.checked, - fetchUrlTitle: this.refs.editorFetchUrlTitle.checked + fetchUrlTitle: this.refs.editorFetchUrlTitle.checked, + enableTableEditor: this.refs.enableTableEditor.checked }, preview: { fontSize: this.refs.previewFontSize.value, @@ -480,6 +481,17 @@ class UiTab extends React.Component { +
+ +
+
{i18n.__('Preview')}
diff --git a/locales/en.json b/locales/en.json index 021c0681..54d98991 100644 --- a/locales/en.json +++ b/locales/en.json @@ -179,5 +179,6 @@ "Save tags of a note in alphabetical order": "Save tags of a note in alphabetical order", "Show tags of a note in alphabetical order": "Show tags of a note in alphabetical order", "Enable live count of notes": "Enable live count of notes", + "Enable smart table editor": "Enable smart table editor", "Snippet Default Language": "Snippet Default Language" } diff --git a/locales/fr.json b/locales/fr.json index 01e24093..3e2ef4a9 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -156,5 +156,6 @@ "Save tags of a note in alphabetical order": "Sauvegarder les tags d'une note en ordre alphabétique", "Show tags of a note in alphabetical order": "Afficher les tags d'une note par ordre alphabétique", "Enable live count of notes": "Activer le comptage live des notes", + "Enable smart table editor": "Activer l'intelligent éditeur de tableaux", "Snippet Default Language": "Langage par défaut d'un snippet" }