diff --git a/server/public/css/codemirror-foldgutter.css b/server/public/css/codemirror-foldgutter.css new file mode 100644 index 00000000..380aba4f --- /dev/null +++ b/server/public/css/codemirror-foldgutter.css @@ -0,0 +1,21 @@ +.CodeMirror-foldmarker { + color: #6f6f6f; + padding-left: 5px; + text-shadow: #6f6f6f 0px 0px 2px; + font-family: arial; + line-height: .3; + cursor: pointer; +} +.CodeMirror-foldgutter { + width: .7em; +} +.CodeMirror-foldgutter-open, +.CodeMirror-foldgutter-folded { + cursor: pointer; +} +.CodeMirror-foldgutter-open:after { + content: "\25BE"; +} +.CodeMirror-foldgutter-folded:after { + content: "\25B8"; +} diff --git a/server/public/css/codemirror.css b/server/public/css/codemirror.css index 0866a2c8..3baa632e 100644 --- a/server/public/css/codemirror.css +++ b/server/public/css/codemirror.css @@ -137,7 +137,11 @@ /* DEFAULT THEME */ -.cm-s-default .cm-header {color: #3E7AA6;} +.cm-s-default .cm-header {color: #3E7AA6; font-size: 1.15em;} +.cm-s-default .cm-header.cm-org-level-star{color: #6f6f6f; position: relative; top: 2px;} +.cm-s-default .cm-header.cm-org-todo{color: #FF8355;} +.cm-s-default .cm-header.cm-org-done{color: #3BB27C;} + .cm-s-default .cm-link{color: #555!important;} .cm-s-default .cm-url{color: #555!important;} .cm-s-default .cm-quote {color: #090;} diff --git a/src/components/editor.js b/src/components/editor.js index 21d68820..39b6dd0b 100644 --- a/src/components/editor.js +++ b/src/components/editor.js @@ -2,37 +2,112 @@ import React from 'react'; import PropTypes from 'prop-types'; import CodeMirror from 'codemirror/lib/codemirror'; + +// keybinding import 'codemirror/keymap/emacs.js'; -import 'codemirror/addon/mode/simple'; + + +// search import 'codemirror/addon/search/searchcursor.js'; import 'codemirror/addon/search/search.js'; import 'codemirror/addon/edit/matchbrackets.js'; import 'codemirror/addon/comment/comment.js'; import 'codemirror/addon/dialog/dialog.js'; -//import '../pages/editpage/javascript'; + +// code folding +import 'codemirror/addon/fold/foldcode'; +import 'codemirror/addon/fold/foldgutter'; + +// modes +import 'codemirror/addon/mode/simple'; CodeMirror.defineSimpleMode("orgmode", { start: [ + {regex: /^(^\*{1,6}\s)(TODO|DOING|WAITING){0,1}(CANCEL|DEFERRED|DONE){0,1}(.*)$/, token: ["header org-level-star", "header org-todo", "header org-done", "header"]}, {regex: /(^\+[^\/]*\+)/, token: ["strikethrough"]}, - {regex: /(^\*[^\/]*\*)/, token: ["header", "strong"]}, + {regex: /(^\*[^\/]*\*)/, token: ["strong"]}, {regex: /(^\/[^\/]*\/)/, token: ["em"]}, {regex: /(^\_[^\/]*\_)/, token: ["link"]}, {regex: /(^\~[^\/]*\~)/, token: ["comment"]}, - {regex: /(^\=[^\/]*\=)/, token: ["comment"]}, - {regex: /(^[\*]+)(\s[TODO|NEXT|DONE|DEFERRED|REJECTED|WAITING]{2,})?(.*)/, token: ['comment', 'qualifier', 'header']}, // headline - {regex: /\s*\:?[A-Z_]+\:.*/, token: "qualifier"}, // property drawers - {regex: /(\#\+[A-Z_]*)(\:.*)/, token: ["keyword", 'qualifier']}, // environments + {regex: /(^\=[^\/]*\=)/, token: ["comment"]}, + // special syntax + //{regex: /(^[\*]+)(\s[TODO|NEXT|DONE|DEFERRED|REJECTED|WAITING]{2,})?(.*)/, token: ['comment', 'qualifier', 'header']}, // headline {regex: /\[\[[^\[\]]*\]\[[^\[\]]*\]\]/, token: "url"}, // links {regex: /\[[xX\s]?\]/, token: 'qualifier'}, // checkbox {regex: /\#\+BEGIN_[A-Z]*/, token: "comment", next: "env"}, // comments + {regex: /:?[A-Z_]+\:.*/, token: "comment"}, // property drawers + {regex: /(\#\+[A-Z_]*)(\:.*)/, token: ["keyword", 'qualifier']}, // environments + {regex: /(CLOCK\:|SHEDULED\:)(\s.+)/, token: ["comment", "keyword"]} ], env: [ {regex: /.*?\#\+END_[A-Z]*/, token: "comment", next: "start"}, {regex: /.*/, token: "comment"} - ], - meta: { - dontIndentStates: ["comment"], - lineComment: "//" + ] +}); +CodeMirror.registerHelper("fold", "orgmode", function(cm, start) { + // init + const levelToMatch = headerLevel(start.line); + + // no folding needed + if(levelToMatch === null) return; + + // find folding limits + const lastLine = cm.lastLine(); + let end = start.line; + while(end < lastLine){ + end += 1; + let level = headerLevel(end); + if(level && level <= levelToMatch) { + end = end - 1; + break; + }; + } + + return { + from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), + to: CodeMirror.Pos(end, cm.getLine(end).length) + }; + + function headerLevel(lineNo) { + var line = cm.getLine(lineNo); + var match = /^\*+/.exec(line); + if(match && match.length === 1 && /header/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)))){ + return match[0].length; + } + return null; + } +}); +CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) { + return mode.name === 'orgmode' ? true : false; +}, function(cm, start) { + const drawer = isBeginningOfADrawer(start.line); + if(drawer === false) return; + + // find folding limits + const lastLine = cm.lastLine(); + let end = start.line; + while(end < lastLine){ + end += 1; + if(isEndOfADrawer(end)){ + break + } + } + return { + from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), + to: CodeMirror.Pos(end, cm.getLine(end).length) + }; + + function isBeginningOfADrawer(lineNo) { + var line = cm.getLine(lineNo); + var match = /^\:.*\:$/.exec(line); + if(match && match.length === 1 && match[0] !== ':END:'){ + return true; + } + return false; + } + function isEndOfADrawer(lineNo){ + var line = cm.getLine(lineNo); + return line.trim() === ':END:' ? true : false; } }); @@ -60,17 +135,91 @@ export class Editor extends React.Component { .then(loadCodeMirror.bind(this)) function loadCodeMirror(mode){ - //console.log(mode) + const size_small = 500; let editor = CodeMirror(document.getElementById('editor'), { value: this.props.content, - lineNumbers: document.body.offsetWidth > 500 ? true : false, + lineNumbers: document.body.offsetWidth > size_small ? true : false, mode: mode, + keyMap: "emacs", lineWrapping: true, - keyMap: "emacs" + foldGutter: { + minFoldSize: 1 + }, + gutters: document.body.offsetWidth > size_small ? ["CodeMirror-linenumbers", "CodeMirror-foldgutter"] : [] }); + if(mode === 'orgmode'){ + let state = { + stab: 'OVERVIEW' + }; + editor.setOption("extraKeys", { + "Tab": function(cm) { + let pos = cm.getCursor(); + isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos); + }, + "Shift-Tab": function(cm){ + if(state.stab === "SHOW_ALL"){ + // fold everything that can be fold + state.stab = 'OVERVIEW'; + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){ + fold(cm, CodeMirror.Pos(i, 0)); + } + }); + }else{ + // unfold all headers + state.stab = 'SHOW_ALL'; + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){ + if(/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true){ + unfold(cm, CodeMirror.Pos(i, 0)) + } + } + }); + } + } + }); + + function fold(cm, start){ + cm.foldCode(start, null, "fold"); + } + function unfold(cm, start){ + cm.foldCode(start, null, "unfold"); + } + function isFold(cm, start){ + const line = start.line; + const marks = cm.findMarks(CodeMirror.Pos(line, 0), CodeMirror.Pos(line + 1, 0)); + for (let i = 0; i < marks.length; ++i) + if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i]; + return false; + } + editor.on('touchstart', function(cm, e){ + setTimeout(() => { + isFold(cm, cm.getCursor()) ? unfold(cm, cm.getCursor()) : fold(cm, cm.getCursor()) + }, 150); + }); + // fold everything except headers by default + editor.operation(function() { + for (var i = 0; i < editor.lineCount() ; i++) { + if(/header/.test(editor.getTokenTypeAt(CodeMirror.Pos(i, 0))) === false){ + fold(editor, CodeMirror.Pos(i, 0)); + } + } + }); + + function collapseWidget(){ + let $widget = document.createElement('span'); + $widget.appendChild(document.createTextNode('colapse')); + return $widget; + } + function expandWidget(){ + let $widget = document.createElement('span'); + $widget.appendChild(document.createTextNode('expand')); + return $widget; + } + } this.setState({editor: editor}); this.updateHeight(this.props.height); - + editor.on('change', (edit) => { if(this.props.onChange){ this.props.onChange(edit.getValue()); @@ -86,7 +235,7 @@ export class Editor extends React.Component { } componentWillUnmount(){ - this.state.editor.clearHistory(); + this.state.editor.clearHistory(); } updateHeight(height){ @@ -99,9 +248,9 @@ export class Editor extends React.Component { loadMode(file){ let ext = file.split('.').pop(), mode = null; - + ext = ext.replace(/~$/, ''); // remove emacs mark when a file is opened - + if(ext === 'org' || ext === 'org_archive'){ return Promise.resolve('orgmode'); } else if(ext === 'js' || ext === 'json'){ // import('../pages/editpage/index') @@ -115,7 +264,7 @@ export class Editor extends React.Component { // }, function(err){ // console.log(err) // }); - + // // return System.import('../pages/editpage/index') // .then((mode) => { @@ -147,9 +296,9 @@ export class Editor extends React.Component { // else if(ext === 'c' || ext === 'cpp' || ext === 'java'){ // mode = 'clike'; // } - else{ return Promise.resolve('orgmode') } + else{ return Promise.resolve('orgmode') } } - + render() { return (
diff --git a/src/index.html b/src/index.html index 663d2553..8b476b15 100644 --- a/src/index.html +++ b/src/index.html @@ -16,6 +16,7 @@ +