feature (folding): org mode syntax highlight and code folding

This commit is contained in:
Mickael KERJEAN 2017-06-20 17:39:21 +10:00
parent c6678a02f1
commit 386eab16fc
4 changed files with 197 additions and 22 deletions

View file

@ -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";
}

View file

@ -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;}

View file

@ -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 (
<div id="editor" style={{height: '100%'}}></div>

View file

@ -16,6 +16,7 @@
<body>
<div id="main"></div>
<link rel="stylesheet" href="/css/codemirror.css">
<link rel="stylesheet" href="/css/codemirror-foldgutter.css">
<link rel="stylesheet" href="/css/videojs-sublime-skin.css">
<link rel="stylesheet" href="/css/video-js.css">
</body>