0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 22:52:40 +00:00

Add code folding feature for all content within a single page

Added the gutter definitions and css for code folding. Enabling code folding in the editor was
tricky due to how CodeMirror loads its files. At the moment, the CodeMirror code-folding code has
been copied into the fold-code.js file. Additionally, that file contains the helper registration
for the Homebrewery-specific code folding function.

#629
This commit is contained in:
Charlie Humphreys
2021-11-02 22:40:17 -05:00
parent 09ca2a4fd9
commit c0b9f4488f
3 changed files with 356 additions and 2 deletions

View File

@@ -13,6 +13,10 @@ if(typeof navigator !== 'undefined'){
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
require('codemirror/mode/css/css.js');
require('codemirror/mode/javascript/javascript.js');
const foldCode = require('./fold-code');
foldCode.enableCodeFolding(CodeMirror);
foldCode.registerHomebreweryHelper(CodeMirror);
}
const CodeEditor = createClass({
@@ -74,8 +78,15 @@ const CodeEditor = createClass({
'Ctrl-M' : this.makeSpan,
'Cmd-M' : this.makeSpan,
'Ctrl-/' : this.makeComment,
'Cmd-/' : this.makeComment
}
'Cmd-/' : this.makeComment,
'Ctrl-,' : this.toggleCodeFolded,
'Cmd-,' : this.toggleCodeFolded
},
foldGutter : true,
foldOptions : {
rangeFinder : CodeMirror.fold.homebrewery,
},
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
});
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
@@ -119,6 +130,10 @@ const CodeEditor = createClass({
}
},
toggleCodeFolded : function() {
this.codeMirror.foldCode(this.codeMirror.getCursor());
},
//=-- Externally used -==//
setCursorPosition : function(line, char){
setTimeout(()=>{

View File

@@ -1,4 +1,5 @@
@import (less) 'codemirror/lib/codemirror.css';
@import (less) 'codemirror/addon/fold/foldgutter.css';
.codeEditor{

View File

@@ -0,0 +1,338 @@
/* eslint-disable max-lines */
module.exports = {
enableCodeFolding : function (CodeMirror) {
// foldcode.js
const makeWidget = function(cm, options, range) {
let widget = getOption(cm, options, 'widget');
if(typeof widget == 'function') {
widget = widget(range.from, range.to);
}
if(typeof widget == 'string') {
const text = document.createTextNode(widget);
widget = document.createElement('span');
widget.appendChild(text);
widget.className = 'CodeMirror-foldmarker';
} else if(widget) {
widget = widget.cloneNode(true);
}
return widget;
};
const doFold = function(cm, pos, options, force) {
let finder;
if(options && options.call) {
finder = options;
options = null;
} else {
finder = getOption(cm, options, 'rangeFinder');
}
if(typeof pos == 'number') pos = CodeMirror.Pos(pos, 0);
const minSize = getOption(cm, options, 'minFoldSize');
const getRange = function(allowFolded) {
const range = finder(cm, pos);
if(!range || range.to.line - range.from.line < minSize) return null;
if(force === 'fold') return range;
const marks = cm.findMarksAt(range.from);
for (let i = 0; i < marks.length; ++i) {
if(marks[i].__isFold) {
if(!allowFolded) return null;
range.cleared = true;
marks[i].clear();
}
}
return range;
};
let range = getRange(true);
if(getOption(cm, options, 'scanUp')) while (!range && pos.line > cm.firstLine()) {
pos = CodeMirror.Pos(pos.line - 1, 0);
range = getRange(false);
}
if(!range || range.cleared || force === 'unfold') return;
const myWidget = makeWidget(cm, options, range);
CodeMirror.on(myWidget, 'mousedown', function (e) {
myRange.clear();
CodeMirror.e_preventDefault(e);
});
const myRange = cm.markText(range.from, range.to, {
replacedWith : myWidget,
clearOnEnter : getOption(cm, options, 'clearOnEnter'),
__isFold : true
});
myRange.on('clear', function (from, to) {
CodeMirror.signal(cm, 'unfold', cm, from, to);
});
CodeMirror.signal(cm, 'fold', cm, range.from, range.to);
};
// Clumsy backwards-compatible interface
CodeMirror.newFoldFunction = function (rangeFinder, widget) {
return function (cm, pos) {
doFold(cm, pos, { rangeFinder: rangeFinder, widget: widget });
};
};
// New-style interface
CodeMirror.defineExtension('foldCode', function (pos, options, force) {
doFold(this, pos, options, force);
});
CodeMirror.defineExtension('isFolded', function (pos) {
const marks = this.findMarksAt(pos);
for (let i = 0; i < marks.length; ++i)
if(marks[i].__isFold) return true;
});
CodeMirror.commands.toggleFold = function (cm) {
cm.foldCode(cm.getCursor());
};
CodeMirror.commands.fold = function (cm) {
cm.foldCode(cm.getCursor(), null, 'fold');
};
CodeMirror.commands.unfold = function (cm) {
cm.foldCode(cm.getCursor(), { scanUp: false }, 'unfold');
};
CodeMirror.commands.foldAll = function (cm) {
cm.operation(function () {
for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, 'fold');
});
};
CodeMirror.commands.unfoldAll = function (cm) {
cm.operation(function () {
for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, 'unfold');
});
};
CodeMirror.registerHelper('fold', 'combine', function () {
const funcs = Array.prototype.slice.call(arguments, 0);
return function (cm, start) {
for (let i = 0; i < funcs.length; ++i) {
const found = funcs[i](cm, start);
if(found) return found;
}
};
});
CodeMirror.registerHelper('fold', 'auto', function (cm, start) {
const helpers = cm.getHelpers(start, 'fold');
for (let i = 0; i < helpers.length; i++) {
const cur = helpers[i](cm, start);
if(cur) return cur;
}
});
const defaultOptions = {
rangeFinder : CodeMirror.fold.auto,
widget : '\u2194',
minFoldSize : 0,
scanUp : false,
clearOnEnter : true
};
CodeMirror.defineOption('foldOptions', null);
const getOption = function(cm, options, name) {
if(options && options[name] !== undefined)
return options[name];
const editorOptions = cm.options.foldOptions;
if(editorOptions && editorOptions[name] !== undefined)
return editorOptions[name];
return defaultOptions[name];
};
CodeMirror.defineExtension('foldOption', function (options, name) {
return getOption(this, options, name);
});
// foldgutter.js
const State = function(options) {
this.options = options;
this.from = this.to = 0;
};
const parseOptions = function(opts) {
if(opts === true) opts = {};
if(opts.gutter == null) opts.gutter = 'CodeMirror-foldgutter';
if(opts.indicatorOpen == null) opts.indicatorOpen = 'CodeMirror-foldgutter-open';
if(opts.indicatorFolded == null) opts.indicatorFolded = 'CodeMirror-foldgutter-folded';
return opts;
};
CodeMirror.defineOption('foldGutter', false, function (cm, val, old) {
if(old && old != CodeMirror.Init) {
cm.clearGutter(cm.state.foldGutter.options.gutter);
cm.state.foldGutter = null;
cm.off('gutterClick', onGutterClick);
cm.off('changes', onChange);
cm.off('viewportChange', onViewportChange);
cm.off('fold', onFold);
cm.off('unfold', onFold);
cm.off('swapDoc', onChange);
}
if(val) {
cm.state.foldGutter = new State(parseOptions(val));
updateInViewport(cm);
cm.on('gutterClick', onGutterClick);
cm.on('changes', onChange);
cm.on('viewportChange', onViewportChange);
cm.on('fold', onFold);
cm.on('unfold', onFold);
cm.on('swapDoc', onChange);
}
});
const Pos = CodeMirror.Pos;
const isFolded = function(cm, line) {
const marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
for (let i = 0; i < marks.length; ++i) {
if(marks[i].__isFold) {
const fromPos = marks[i].find(-1);
if(fromPos && fromPos.line === line)
return marks[i];
}
}
};
const marker = function(spec) {
if(typeof spec == 'string') {
const elt = document.createElement('div');
elt.className = `${spec} CodeMirror-guttermarker-subtle`;
return elt;
} else {
return spec.cloneNode(true);
}
};
const updateFoldInfo = function(cm, from, to) {
const opts = cm.state.foldGutter.options;
let cur = from - 1;
const minSize = cm.foldOption(opts, 'minFoldSize');
const func = cm.foldOption(opts, 'rangeFinder');
// we can reuse the built-in indicator element if its className matches the new state
const clsFolded = typeof opts.indicatorFolded == 'string' && classTest(opts.indicatorFolded);
const clsOpen = typeof opts.indicatorOpen == 'string' && classTest(opts.indicatorOpen);
cm.eachLine(from, to, function (line) {
++cur;
let mark = null;
let old = line.gutterMarkers;
if(old) old = old[opts.gutter];
if(isFolded(cm, cur)) {
if(clsFolded && old && clsFolded.test(old.className)) return;
mark = marker(opts.indicatorFolded);
} else {
const pos = Pos(cur, 0);
const range = func && func(cm, pos);
if(range && range.to.line - range.from.line >= minSize) {
if(clsOpen && old && clsOpen.test(old.className)) return;
mark = marker(opts.indicatorOpen);
}
}
if(!mark && !old) return;
cm.setGutterMarker(line, opts.gutter, mark);
});
};
// copied from CodeMirror/src/util/dom.js
const classTest = function(cls) {
return new RegExp(`(^|\\s)${cls}(?:$|\\s)\\s*`);
};
const updateInViewport = function(cm) {
const vp = cm.getViewport(), state = cm.state.foldGutter;
if(!state) return;
cm.operation(function () {
updateFoldInfo(cm, vp.from, vp.to);
});
state.from = vp.from;
state.to = vp.to;
};
const onGutterClick = function(cm, line, gutter) {
const state = cm.state.foldGutter;
if(!state) return;
const opts = state.options;
if(gutter != opts.gutter) return;
const folded = isFolded(cm, line);
if(folded) folded.clear();
else cm.foldCode(Pos(line, 0), opts);
};
const onChange = function(cm) {
const state = cm.state.foldGutter;
if(!state) return;
const opts = state.options;
state.from = state.to = 0;
clearTimeout(state.changeUpdate);
state.changeUpdate = setTimeout(function () {
updateInViewport(cm);
}, opts.foldOnChangeTimeSpan || 600);
};
const onViewportChange = function(cm) {
const state = cm.state.foldGutter;
if(!state) return;
const opts = state.options;
clearTimeout(state.changeUpdate);
state.changeUpdate = setTimeout(function () {
const vp = cm.getViewport();
if(state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
updateInViewport(cm);
} else {
cm.operation(function () {
if(vp.from < state.from) {
updateFoldInfo(cm, vp.from, state.from);
state.from = vp.from;
}
if(vp.to > state.to) {
updateFoldInfo(cm, state.to, vp.to);
state.to = vp.to;
}
});
}
}, opts.updateViewportTimeSpan || 400);
};
const onFold = function(cm, from) {
const state = cm.state.foldGutter;
if(!state) return;
const line = from.line;
if(line >= state.from && line < state.to)
updateFoldInfo(cm, line, line + 1);
};
},
registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) {
const matcher = /^\\page.*/;
const firstLine = cm.getLine(start.line);
const prevLine = cm.getLine(start.line - 1);
if(start.line === cm.firstLine() || prevLine.match(matcher)) {
const lastLineNo = cm.lastLine();
let end = start.line, nextLine = cm.getLine(start.line + 1);
while (end < lastLineNo) {
if(nextLine.match(matcher)) {
return {
from : CodeMirror.Pos(start.line, firstLine.length),
to : CodeMirror.Pos(end, cm.getLine(end).length)
};
}
++end;
nextLine = cm.getLine(end + 1);
}
return null;
}
return null;
});
}
};