From c0b9f4488f10cde00c695f26e183b3a15367707c Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Tue, 2 Nov 2021 22:40:17 -0500 Subject: [PATCH 01/11] 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 --- shared/naturalcrit/codeEditor/codeEditor.jsx | 19 +- shared/naturalcrit/codeEditor/codeEditor.less | 1 + shared/naturalcrit/codeEditor/fold-code.js | 338 ++++++++++++++++++ 3 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 shared/naturalcrit/codeEditor/fold-code.js diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index 621e24f77..68fdea3b4 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -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(()=>{ diff --git a/shared/naturalcrit/codeEditor/codeEditor.less b/shared/naturalcrit/codeEditor/codeEditor.less index 7daa0c4da..989c2f469 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.less +++ b/shared/naturalcrit/codeEditor/codeEditor.less @@ -1,4 +1,5 @@ @import (less) 'codemirror/lib/codemirror.css'; +@import (less) 'codemirror/addon/fold/foldgutter.css'; .codeEditor{ diff --git a/shared/naturalcrit/codeEditor/fold-code.js b/shared/naturalcrit/codeEditor/fold-code.js new file mode 100644 index 000000000..71e2ff699 --- /dev/null +++ b/shared/naturalcrit/codeEditor/fold-code.js @@ -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; + }); + } +}; \ No newline at end of file From eec6e66543eeacf484a4487bcaef76f88dc2f26b Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Sun, 7 Nov 2021 21:35:35 -0600 Subject: [PATCH 02/11] Replace copied code with require methods to import the CodeMirror helpers Put back the correct require notation for importing the foldcode and foldgutter helpers #629 --- shared/naturalcrit/codeEditor/codeEditor.jsx | 47 ++- shared/naturalcrit/codeEditor/codeEditor.less | 4 +- shared/naturalcrit/codeEditor/fold-code.js | 311 +----------------- 3 files changed, 46 insertions(+), 316 deletions(-) diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index 68fdea3b4..7e2571ce0 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -14,8 +14,11 @@ if(typeof navigator !== 'undefined'){ require('codemirror/mode/css/css.js'); require('codemirror/mode/javascript/javascript.js'); + // this should add the foldcode helpers and such to the CodeMirror object, but seemingly is adding them to a new instance of CodeMirror + require('codemirror/addon/fold/foldcode.js'); + require('codemirror/addon/fold/foldgutter.js'); + const foldCode = require('./fold-code'); - foldCode.enableCodeFolding(CodeMirror); foldCode.registerHomebreweryHelper(CodeMirror); } @@ -40,8 +43,10 @@ const CodeEditor = createClass({ const newDoc = CodeMirror.Doc(this.props.value, this.props.language); this.codeMirror.swapDoc(newDoc); }, - - componentDidUpdate : function(prevProps) { + getSnapshotBeforeUpdate : function() { + return _.uniq(this.codeMirror.getAllMarks().filter((mark)=>mark.__isFold).map((mark)=>mark.find().from)); + }, + componentDidUpdate : function(prevProps, prevState, snapshot) { if(prevProps.view !== this.props.view){ //view changed; swap documents let newDoc; @@ -61,6 +66,12 @@ const CodeEditor = createClass({ } else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside this.codeMirror.setValue(this.props.value); } + + setTimeout(()=>{ + snapshot.forEach((fold)=>{ + this.codeMirror.foldCode(fold, { scanUp: false }, 'fold'); + }); + }, 0); }, buildEditor : function() { @@ -79,12 +90,30 @@ const CodeEditor = createClass({ 'Cmd-M' : this.makeSpan, 'Ctrl-/' : this.makeComment, 'Cmd-/' : this.makeComment, - 'Ctrl-,' : this.toggleCodeFolded, - 'Cmd-,' : this.toggleCodeFolded + 'Ctrl-\\' : this.toggleCodeFolded, + 'Cmd-\\' : this.toggleCodeFolded, + 'Ctrl-[' : this.foldAllCode, + 'Cmd-[' : this.foldAllCode, + 'Ctrl-]' : this.unfoldAllCode, + 'Cmd-]' : this.unfoldAllCode }, foldGutter : true, foldOptions : { rangeFinder : CodeMirror.fold.homebrewery, + widget : (from, to)=>{ + let text = ''; + let currentLine = from.line; + const maxLength = 50; + while (currentLine <= to.line && text.length <= maxLength) { + text += this.codeMirror.getLine(currentLine); + if(currentLine < to.line) { + text += ' '; + } + currentLine += 1; + } + + return `\u21A4${text.substr(0, maxLength)}\u21A6`; + } }, gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] }); @@ -134,6 +163,14 @@ const CodeEditor = createClass({ this.codeMirror.foldCode(this.codeMirror.getCursor()); }, + foldAllCode : function() { + this.codeMirror.execCommand('foldAll'); + }, + + unfoldAllCode : function() { + this.codeMirror.execCommand('unfoldAll'); + }, + //=-- Externally used -==// setCursorPosition : function(line, char){ setTimeout(()=>{ diff --git a/shared/naturalcrit/codeEditor/codeEditor.less b/shared/naturalcrit/codeEditor/codeEditor.less index 989c2f469..5c0680349 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.less +++ b/shared/naturalcrit/codeEditor/codeEditor.less @@ -2,5 +2,7 @@ @import (less) 'codemirror/addon/fold/foldgutter.css'; .codeEditor{ - + .CodeMirror-foldmarker { + font-family: inherit; + } } \ No newline at end of file diff --git a/shared/naturalcrit/codeEditor/fold-code.js b/shared/naturalcrit/codeEditor/fold-code.js index 71e2ff699..1799fe5d8 100644 --- a/shared/naturalcrit/codeEditor/fold-code.js +++ b/shared/naturalcrit/codeEditor/fold-code.js @@ -1,313 +1,4 @@ -/* 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.*/; @@ -321,7 +12,7 @@ module.exports = { while (end < lastLineNo) { if(nextLine.match(matcher)) { return { - from : CodeMirror.Pos(start.line, firstLine.length), + from : CodeMirror.Pos(start.line, 0), to : CodeMirror.Pos(end, cm.getLine(end).length) }; } From f3d0d3e2c902c0752cf7702801ccb9c1388561c3 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Mon, 8 Nov 2021 17:24:22 -0600 Subject: [PATCH 03/11] Add new library files to the browserify lib array #629 --- scripts/project.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/project.json b/scripts/project.json index 9bb6ef1ed..a83b7a0a1 100644 --- a/scripts/project.json +++ b/scripts/project.json @@ -12,6 +12,8 @@ "codemirror/mode/gfm/gfm.js", "codemirror/mode/css/css.js", "codemirror/mode/javascript/javascript.js", + "codemirror/addon/fold/foldcode.js", + "codemirror/addon/fold/foldgutter.js", "moment", "superagent", "marked" From b06dedfa4af3301e01c9041e5bad75329da2f813 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Mon, 8 Nov 2021 17:24:33 -0600 Subject: [PATCH 04/11] Fix linting issues #629 --- shared/naturalcrit/codeEditor/codeEditor.jsx | 24 ++++++++++---------- shared/naturalcrit/codeEditor/fold-code.js | 1 - 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index 7e2571ce0..2bc2b043e 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -82,20 +82,20 @@ const CodeEditor = createClass({ tabSize : 2, historyEventDelay : 250, extraKeys : { - 'Ctrl-B' : this.makeBold, - 'Cmd-B' : this.makeBold, - 'Ctrl-I' : this.makeItalic, - 'Cmd-I' : this.makeItalic, - 'Ctrl-M' : this.makeSpan, - 'Cmd-M' : this.makeSpan, - 'Ctrl-/' : this.makeComment, - 'Cmd-/' : this.makeComment, + 'Ctrl-B' : this.makeBold, + 'Cmd-B' : this.makeBold, + 'Ctrl-I' : this.makeItalic, + 'Cmd-I' : this.makeItalic, + 'Ctrl-M' : this.makeSpan, + 'Cmd-M' : this.makeSpan, + 'Ctrl-/' : this.makeComment, + 'Cmd-/' : this.makeComment, 'Ctrl-\\' : this.toggleCodeFolded, 'Cmd-\\' : this.toggleCodeFolded, - 'Ctrl-[' : this.foldAllCode, - 'Cmd-[' : this.foldAllCode, - 'Ctrl-]' : this.unfoldAllCode, - 'Cmd-]' : this.unfoldAllCode + 'Ctrl-[' : this.foldAllCode, + 'Cmd-[' : this.foldAllCode, + 'Ctrl-]' : this.unfoldAllCode, + 'Cmd-]' : this.unfoldAllCode }, foldGutter : true, foldOptions : { diff --git a/shared/naturalcrit/codeEditor/fold-code.js b/shared/naturalcrit/codeEditor/fold-code.js index 1799fe5d8..ea9102de6 100644 --- a/shared/naturalcrit/codeEditor/fold-code.js +++ b/shared/naturalcrit/codeEditor/fold-code.js @@ -2,7 +2,6 @@ module.exports = { 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)) { From 3abb399045a77d388cbff61d664b1967e8588c2c Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Tue, 9 Nov 2021 10:08:48 -0600 Subject: [PATCH 05/11] Update homebrewery fold helper to allow folding the last page to the end of the document #629 --- shared/naturalcrit/codeEditor/fold-code.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shared/naturalcrit/codeEditor/fold-code.js b/shared/naturalcrit/codeEditor/fold-code.js index ea9102de6..f7e8b0901 100644 --- a/shared/naturalcrit/codeEditor/fold-code.js +++ b/shared/naturalcrit/codeEditor/fold-code.js @@ -19,7 +19,10 @@ module.exports = { nextLine = cm.getLine(end + 1); } - return null; + return { + from : CodeMirror.Pos(start.line, 0), + to : CodeMirror.Pos(end, cm.getLine(end).length) + }; } return null; From c86e8c51cbdcdfa4b96c19e56c38571b35d12178 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Wed, 10 Nov 2021 10:26:55 -0600 Subject: [PATCH 06/11] Remove code added for debugging purposes #629 --- shared/naturalcrit/codeEditor/codeEditor.jsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index 2bc2b043e..6d352e854 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -43,10 +43,8 @@ const CodeEditor = createClass({ const newDoc = CodeMirror.Doc(this.props.value, this.props.language); this.codeMirror.swapDoc(newDoc); }, - getSnapshotBeforeUpdate : function() { - return _.uniq(this.codeMirror.getAllMarks().filter((mark)=>mark.__isFold).map((mark)=>mark.find().from)); - }, - componentDidUpdate : function(prevProps, prevState, snapshot) { + + componentDidUpdate : function(prevProps) { if(prevProps.view !== this.props.view){ //view changed; swap documents let newDoc; @@ -66,12 +64,6 @@ const CodeEditor = createClass({ } else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside this.codeMirror.setValue(this.props.value); } - - setTimeout(()=>{ - snapshot.forEach((fold)=>{ - this.codeMirror.foldCode(fold, { scanUp: false }, 'fold'); - }); - }, 0); }, buildEditor : function() { From cac9e208df00aab6e9c5cc434811fce809e6ccb5 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Wed, 10 Nov 2021 10:27:32 -0600 Subject: [PATCH 07/11] Update editor.jsx to only clear marks that are not folds #629 --- client/homebrew/editor/editor.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index 77001435d..61d330772 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -108,7 +108,7 @@ const Editor = createClass({ const codeMirror = this.refs.codeEditor.codeMirror; //reset custom text styles - const customHighlights = codeMirror.getAllMarks(); + const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); for (let i=0;i{ From 52d7e6892b749d90f7b7b79192b6eb89e6337767 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Wed, 10 Nov 2021 11:37:18 -0600 Subject: [PATCH 08/11] Adjust comment to better describe the require section #629 --- shared/naturalcrit/codeEditor/codeEditor.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index 6d352e854..3fdfec252 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -14,7 +14,7 @@ if(typeof navigator !== 'undefined'){ require('codemirror/mode/css/css.js'); require('codemirror/mode/javascript/javascript.js'); - // this should add the foldcode helpers and such to the CodeMirror object, but seemingly is adding them to a new instance of CodeMirror + //Addons require('codemirror/addon/fold/foldcode.js'); require('codemirror/addon/fold/foldgutter.js'); From 2d30ac21a761837535803b13a5c2a1d6a5806c18 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Thu, 11 Nov 2021 17:08:05 -0600 Subject: [PATCH 09/11] Update code based on PR feedback #692 --- client/homebrew/editor/editor.jsx | 2 +- shared/naturalcrit/codeEditor/codeEditor.jsx | 10 +++++++--- shared/naturalcrit/codeEditor/codeEditor.less | 2 ++ shared/naturalcrit/codeEditor/fold-code.js | 8 ++------ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index 61d330772..14038f740 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -108,7 +108,7 @@ const Editor = createClass({ const codeMirror = this.refs.codeEditor.codeMirror; //reset custom text styles - const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); + const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding for (let i=0;i{ diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index 3fdfec252..5190e0b9e 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -91,6 +91,7 @@ const CodeEditor = createClass({ }, foldGutter : true, foldOptions : { + scanUp : true, rangeFinder : CodeMirror.fold.homebrewery, widget : (from, to)=>{ let text = ''; @@ -98,13 +99,16 @@ const CodeEditor = createClass({ const maxLength = 50; while (currentLine <= to.line && text.length <= maxLength) { text += this.codeMirror.getLine(currentLine); - if(currentLine < to.line) { + if(currentLine < to.line) text += ' '; - } currentLine += 1; } - return `\u21A4${text.substr(0, maxLength)}\u21A6`; + text = text.trim(); + if(text.length > maxLength) + text = `${text.substr(0, maxLength)}...`; + + return `\u21A4 ${text} \u21A6`; } }, gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] diff --git a/shared/naturalcrit/codeEditor/codeEditor.less b/shared/naturalcrit/codeEditor/codeEditor.less index 5c0680349..0ad7a27f8 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.less +++ b/shared/naturalcrit/codeEditor/codeEditor.less @@ -4,5 +4,7 @@ .codeEditor{ .CodeMirror-foldmarker { font-family: inherit; + text-shadow: none; + font-weight: 600; } } \ No newline at end of file diff --git a/shared/naturalcrit/codeEditor/fold-code.js b/shared/naturalcrit/codeEditor/fold-code.js index f7e8b0901..e43fda8ec 100644 --- a/shared/naturalcrit/codeEditor/fold-code.js +++ b/shared/naturalcrit/codeEditor/fold-code.js @@ -9,12 +9,8 @@ module.exports = { let end = start.line, nextLine = cm.getLine(start.line + 1); while (end < lastLineNo) { - if(nextLine.match(matcher)) { - return { - from : CodeMirror.Pos(start.line, 0), - to : CodeMirror.Pos(end, cm.getLine(end).length) - }; - } + if(nextLine.match(matcher)) + break; ++end; nextLine = cm.getLine(end + 1); } From b67dc1621bde848d9d340f6b6ad45d0a8e628a20 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 13 Nov 2021 20:57:33 -0500 Subject: [PATCH 10/11] More simplification to fold-code script --- shared/naturalcrit/codeEditor/fold-code.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shared/naturalcrit/codeEditor/fold-code.js b/shared/naturalcrit/codeEditor/fold-code.js index e43fda8ec..cff1c64e0 100644 --- a/shared/naturalcrit/codeEditor/fold-code.js +++ b/shared/naturalcrit/codeEditor/fold-code.js @@ -6,13 +6,12 @@ module.exports = { if(start.line === cm.firstLine() || prevLine.match(matcher)) { const lastLineNo = cm.lastLine(); - let end = start.line, nextLine = cm.getLine(start.line + 1); + let end = start.line; while (end < lastLineNo) { - if(nextLine.match(matcher)) + if(cm.getLine(end + 1).match(matcher)) break; ++end; - nextLine = cm.getLine(end + 1); } return { @@ -24,4 +23,4 @@ module.exports = { return null; }); } -}; \ No newline at end of file +}; From 1ec1ddc80c5036dee78ac7bc534ec7173bb73f6a Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Sun, 14 Nov 2021 09:58:41 -0600 Subject: [PATCH 11/11] Remove code fold toggling shortcut #629 --- shared/naturalcrit/codeEditor/codeEditor.jsx | 30 ++++++++------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index 5190e0b9e..01eca0114 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -74,20 +74,18 @@ const CodeEditor = createClass({ tabSize : 2, historyEventDelay : 250, extraKeys : { - 'Ctrl-B' : this.makeBold, - 'Cmd-B' : this.makeBold, - 'Ctrl-I' : this.makeItalic, - 'Cmd-I' : this.makeItalic, - 'Ctrl-M' : this.makeSpan, - 'Cmd-M' : this.makeSpan, - 'Ctrl-/' : this.makeComment, - 'Cmd-/' : this.makeComment, - 'Ctrl-\\' : this.toggleCodeFolded, - 'Cmd-\\' : this.toggleCodeFolded, - 'Ctrl-[' : this.foldAllCode, - 'Cmd-[' : this.foldAllCode, - 'Ctrl-]' : this.unfoldAllCode, - 'Cmd-]' : this.unfoldAllCode + 'Ctrl-B' : this.makeBold, + 'Cmd-B' : this.makeBold, + 'Ctrl-I' : this.makeItalic, + 'Cmd-I' : this.makeItalic, + 'Ctrl-M' : this.makeSpan, + 'Cmd-M' : this.makeSpan, + 'Ctrl-/' : this.makeComment, + 'Cmd-/' : this.makeComment, + 'Ctrl-[' : this.foldAllCode, + 'Cmd-[' : this.foldAllCode, + 'Ctrl-]' : this.unfoldAllCode, + 'Cmd-]' : this.unfoldAllCode }, foldGutter : true, foldOptions : { @@ -155,10 +153,6 @@ const CodeEditor = createClass({ } }, - toggleCodeFolded : function() { - this.codeMirror.foldCode(this.codeMirror.getCursor()); - }, - foldAllCode : function() { this.codeMirror.execCommand('foldAll'); },