export const org_cycle = (cm) => { const pos = cm.getCursor(); isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos); }; const state = { stab: "CONTENT", }; export const org_set_fold = (cm) => { const cursor = cm.getCursor(); set_folding_mode(cm, state.stab); cm.setCursor(cursor); return state.stab; }; /* * DONE: Global visibility cycling * TODO: or move to previous table field. */ export const org_shifttab = (cm) => { if (state.stab === "SHOW_ALL") { state.stab = "OVERVIEW"; } else if (state.stab === "OVERVIEW") { state.stab = "CONTENT"; } else if (state.stab === "CONTENT") { state.stab = "SHOW_ALL"; } set_folding_mode(cm, state.stab); return state.stab; }; function set_folding_mode(cm, mode) { if (mode === "OVERVIEW") { folding_mode_overview(cm); } else if (mode === "SHOW_ALL") { folding_mode_all(cm); } else if (mode === "CONTENT") { folding_mode_content(cm); } cm.refresh(); function folding_mode_overview(cm) { cm.operation(function() { for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) { fold(cm, CodeMirror.Pos(i, 0)); } }); } function folding_mode_content(cm) { cm.operation(function() { let previous_header = null; for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) { fold(cm, CodeMirror.Pos(i, 0)); if (/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true) { const level = cm.getLine(i).replace(/^(\*+).*/, "$1").length; if (previous_header && level > previous_header.level) { unfold(cm, CodeMirror.Pos(previous_header.line, 0)); } previous_header = { line: i, level: level, }; } } }); } function folding_mode_all(cm) { cm.operation(function() { for (let 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)); } } }); } } /* * Promote heading or move table column to left. */ export const org_metaleft = (cm) => { const line = cm.getCursor().line; _metaleft(cm, line); }; function _metaleft(cm, line) { let p = null; if (p = isTitle(cm, line)) { if (p["level"] > 1) { cm.replaceRange( "", { line: p.start, ch: 0 }, { line: p.start, ch: 1 }, ); } } else if (p = isItemList(cm, line)) { for (let i=p.start; i<=p.end; i++) { if (p["level"] > 0) { cm.replaceRange( "", { line: i, ch: 0 }, { line: i, ch: 2 }, ); } } } else if (p = isNumberedList(cm, line)) { for (let i=p.start; i<=p.end; i++) { if (p["level"] > 0) { cm.replaceRange( "", { line: i, ch: 0 }, { line: i, ch: 3 }, ); } } rearrange_list(cm, line); } } /* * Demote a subtree, a list item or move table column to right. * In front of a drawer or a block keyword, indent it correctly. */ export const org_metaright = (cm) => { const line = cm.getCursor().line; _metaright(cm, line); }; function _metaright(cm, line) { let p = null; let tmp = null; if (p = isTitle(cm, line)) { cm.replaceRange("*", { line: p.start, ch: 0 }); } else if (p = isItemList(cm, line)) { if (tmp = isItemList(cm, p.start - 1)) { if (p.level < tmp.level + 1) { for (let i=p.start; i<=p.end; i++) { cm.replaceRange(" ", { line: i, ch: 0 }); } } } } else if (p = isNumberedList(cm, line)) { if (tmp = isNumberedList(cm, p.start - 1)) { if (p.level < tmp.level + 1) { for (let i=p.start; i<=p.end; i++) { cm.replaceRange(" ", { line: i, ch: 0 }); } rearrange_list(cm, p.start); } } } } /* * Insert a new heading or wrap a region in a table */ export const org_meta_return = (cm) => { const line = cm.getCursor().line; const content = cm.getLine(line); let p = null; if (p = isItemList(cm, line)) { const level = p.level; cm.replaceRange( "\n"+" ".repeat(level*2)+"- ", { line: p.end, ch: cm.getLine(p.end).length }, ); cm.setCursor({ line: p.end+1, ch: level*2+2 }); } else if (p = isNumberedList(cm, line)) { const level = p.level; cm.replaceRange( "\n"+" ".repeat(level*3)+(p.n+1)+". ", { line: p.end, ch: cm.getLine(p.end).length }, ); cm.setCursor({ line: p.end+1, ch: level*3+3 }); rearrange_list(cm, line); } else if (p = isTitle(cm, line)) { const tmp = previousOfType(cm, "title", line); const level = tmp && tmp.level || 1; cm.replaceRange("\n"+"*".repeat(level)+" ", { line: line, ch: content.length }); cm.setCursor({ line: line+1, ch: level+1 }); } else if (content.trim() === "") { cm.replaceRange("* ", { line: line, ch: 0 }); cm.setCursor({ line: line, ch: 2 }); } else { cm.replaceRange("\n\n* ", { line: line, ch: content.length }); cm.setCursor({ line: line + 2, ch: 2 }); } }; const TODO_CYCLES = ["TODO", "DONE", ""]; /* * Cycle the thing at point or in the current line, depending on context. * Depending on context, this does one of the following: * - TODO: switch a timestamp at point one day into the past * - DONE: on a headline, switch to the previous TODO keyword. * - TODO: on an item, switch entire list to the previous bullet type * - TODO: on a property line, switch to the previous allowed value * - TODO: on a clocktable definition line, move time block into the past */ export const org_shiftleft = (cm) => { const cycles = [].concat(TODO_CYCLES.slice(0).reverse(), TODO_CYCLES.slice(-1)); const line = cm.getCursor().line; const content = cm.getLine(line); const params = isTitle(cm, line); if (params === null) return; params["status"] = cycles[cycles.indexOf(params["status"]) + 1]; cm.replaceRange( makeTitle(params), { line: line, ch: 0 }, { line: line, ch: content.length }, ); }; /* * Cycle the thing at point or in the current line, depending on context. * Depending on context, this does one of the following: * - TODO: switch a timestamp at point one day into the future * - DONE: on a headline, switch to the next TODO keyword. * - TODO: on an item, switch entire list to the next bullet type * - TODO: on a property line, switch to the next allowed value * - TODO: on a clocktable definition line, move time block into the future */ export const org_shiftright = (cm) => { cm.operation(() => { const cycles = [].concat(TODO_CYCLES, [TODO_CYCLES[0]]); const line = cm.getCursor().line; const content = cm.getLine(line); const params = isTitle(cm, line); if (params === null) return; params["status"] = cycles[cycles.indexOf(params["status"]) + 1]; cm.replaceRange( makeTitle(params), { line: line, ch: 0 }, { line: line, ch: content.length }, ); }); }; export const org_insert_todo_heading = (cm) => { cm.operation(() => { const line = cm.getCursor().line; const content = cm.getLine(line); let p = null; if (p = isItemList(cm, line)) { const level = p.level; cm.replaceRange( "\n"+" ".repeat(level*2)+"- [ ] ", { line: p.end, ch: cm.getLine(p.end).length }, ); cm.setCursor({ line: line+1, ch: 6+level*2 }); } else if (p = isNumberedList(cm, line)) { const level = p.level; cm.replaceRange( "\n"+" ".repeat(level*3)+(p.n+1)+". [ ] ", { line: p.end, ch: cm.getLine(p.end).length }, ); cm.setCursor({ line: p.end+1, ch: level*3+7 }); rearrange_list(cm, line); } else if (p = isTitle(cm, line)) { const level = p && p.level || 1; cm.replaceRange("\n"+"*".repeat(level)+" TODO ", { line: line, ch: content.length }); cm.setCursor({ line: line+1, ch: level+6 }); } else if (content.trim() === "") { cm.replaceRange("* TODO ", { line: line, ch: 0 }); cm.setCursor({ line: line, ch: 7 }); } else { cm.replaceRange("\n\n* TODO ", { line: line, ch: content.length }); cm.setCursor({ line: line + 2, ch: 7 }); } }); }; /* * Move subtree up or move table row up. * Calls ‘org-move-subtree-up’ or ‘org-table-move-row’ or * ‘org-move-item-up’, depending on context */ export const org_metaup = (cm) => { cm.operation(() => { const line = cm.getCursor().line; let p = null; if (p = isItemList(cm, line)) { const a = isItemList(cm, p.start - 1); if (a) { swap(cm, [p.start, p.end], [a.start, a.end]); rearrange_list(cm, line); } } else if (p = isNumberedList(cm, line)) { const a = isNumberedList(cm, p.start - 1); if (a) { swap(cm, [p.start, p.end], [a.start, a.end]); rearrange_list(cm, line); } } else if (p = isTitle(cm, line)) { let _line = line; let a; do { _line -= 1; if (a = isTitle(cm, _line, p.level)) { break; } } while (_line > 0); if (a) { swap(cm, [p.start, p.end], [a.start, a.end]); org_set_fold(cm); } } }); }; /* * Move subtree down or move table row down. * Calls ‘org-move-subtree-down’ or ‘org-table-move-row’ or * ‘org-move-item-down’, depending on context */ export const org_metadown = (cm) => { cm.operation(() => { const line = cm.getCursor().line; let p = null; if (p = isItemList(cm, line)) { const a = isItemList(cm, p.end + 1); if (a) { swap(cm, [p.start, p.end], [a.start, a.end]); } } else if (p = isNumberedList(cm, line)) { const a = isNumberedList(cm, p.end + 1); if (a) { swap(cm, [p.start, p.end], [a.start, a.end]); } rearrange_list(cm, line); } else if (p = isTitle(cm, line)) { const a = isTitle(cm, p.end + 1, p.level); if (a) { swap(cm, [p.start, p.end], [a.start, a.end]); org_set_fold(cm); } } }); }; export const org_shiftmetaright = function(cm) { cm.operation(() => { const line = cm.getCursor().line; let p = null; if (p = isTitle(cm, line)) { _metaright(cm, line); for (let i=p.start + 1; i<=p.end; i++) { if (isTitle(cm, i)) { _metaright(cm, i); } } } }); }; export const org_shiftmetaleft = function(cm) { cm.operation(() => { const line = cm.getCursor().line; let p = null; if (p = isTitle(cm, line)) { if (p.level === 1) return; _metaleft(cm, line); for (let i=p.start + 1; i<=p.end; i++) { if (isTitle(cm, i)) { _metaleft(cm, i); } } } }); }; function makeTitle(p) { let content = "*".repeat(p["level"])+" "; if (p["status"]) { content += p["status"]+" "; } content += p["content"]; return content; } function previousOfType(cm, type, line) { let tmp; let i; for (i=line - 1; i>0; i--) { if (type === "list" || type === null) { tmp = isItemList(cm, line); } else if (type === "numbered" || type === null) { tmp = isNumberedList(cm, line); } else if (type === "title" || type === null) { tmp = isTitle(cm, line); } if (tmp !== null) { return tmp; } } return null; } function isItemList(cm, line) { const rootLineItem = findRootLine(cm, line); if (rootLineItem === null) return null; line = rootLineItem; const content = cm.getLine(line); if (content && (content.trimLeft()[0] !== "-" || content.trimLeft()[1] !== " ")) return null; const padding = content.replace(/^(\s*).*$/, "$1").length; if (padding % 2 !== 0) return null; return { type: "list", level: padding / 2, content: content.trimLeft().replace(/^\s*\-\s(.*)$/, "$1"), start: line, end: function(_cm, _line) { let line_candidate = _line; let content = null; do { _line += 1; content = _cm.getLine(_line); if (content === undefined || content.trimLeft()[0] === "-") { break; } else if (/^\s+/.test(content)) { line_candidate = _line; continue; } else { break; } } while (_line <= _cm.lineCount()); return line_candidate; }(cm, line), }; function findRootLine(_cm, _line) { let content; do { content = _cm.getLine(_line); if (/^\s*\-/.test(content)) return _line; else if (/^\s+/.test(content) === false) { break; } _line -= 1; } while (_line >= 0); return null; } } function isNumberedList(cm, line) { const rootLineItem = findRootLine(cm, line); if (rootLineItem === null) return null; line = rootLineItem; const content = cm.getLine(line); if (/^[0-9]+[\.\)]\s.*$/.test(content && content.trimLeft()) === false) return null; const padding = content.replace(/^(\s*)[0-9]+.*$/, "$1").length; if (padding % 3 !== 0) return null; return { type: "numbered", level: padding / 3, content: content.trimLeft().replace(/^[0-9]+[\.\)]\s(.*)$/, "$1"), start: line, end: function(_cm, _line) { let line_candidate = _line; let content = null; do { _line += 1; content = _cm.getLine(_line); if (content === undefined || /^[0-9]+[\.\)]/.test(content.trimLeft())) { break; } else if (/^\s+/.test(content)) { line_candidate = _line; continue; } else { break; } } while (_line <= _cm.lineCount()); return line_candidate; }(cm, line), // specific n: parseInt(content.trimLeft().replace(/^([0-9]+).*$/, "$1")), separator: content.trimLeft().replace(/^[0-9]+([\.\)]).*$/, "$1"), }; function findRootLine(_cm, _line) { let content; do { content = _cm.getLine(_line); if (/^\s*[0-9]+[\.\)]\s/.test(content)) return _line; else if (/^\s+/.test(content) === false) { break; } _line -= 1; } while (_line >= 0); return null; } } function isTitle(cm, line, level) { const content = cm.getLine(line); if (/^\*+\s/.test(content) === false) return null; const match = content.match(/^(\*+)([\sA-Z]*)\s(.*)$/); const reference_level = match[1].length; if (level !== undefined && level !== reference_level) return null; if (match === null) return null; return { type: "title", level: reference_level, content: match[3], start: line, end: function(_cm, _line) { let line_candidate = _line; let content = null; do { _line += 1; content = _cm.getLine(_line); if (content === undefined) break; const match = content.match(/^(\*+)\s.*/); if ( match && match[1] && ( match[1].length === reference_level || match[1].length < reference_level) ) { break; } else { line_candidate = _line; continue; } } while (_line <= _cm.lineCount()); return line_candidate; }(cm, line), // specific status: match[2].trim(), }; } function rearrange_list(cm, line) { const line_inferior = find_limit_inferior(cm, line); const line_superior = find_limit_superior(cm, line); let last_p = null; let p; for (let i=line_inferior; i<=line_superior; i++) { if (p = isNumberedList(cm, i)) { // rearrange numbers on the numbered list if (last_p) { if (p.level === last_p.level) { const tmp = findLastAtLevel(cm, p.start, line_inferior, p.level); if (tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1); } else if (p.level > last_p.level) { if (p.n !== 1) { setNumber(cm, p.start, 1); } } else if (p.level < last_p.level) { const tmp = findLastAtLevel(cm, p.start, line_inferior, p.level); if (tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1); } } else { if (p.n !== 1) setNumber(cm, p.start, 1); } } if (p = (isNumberedList(cm, i) || isItemList(cm, i))) { // rearrange spacing levels in list if (last_p) { if (p.level > last_p.level) { if (p.level !== last_p.level + 1) { setLevel(cm, [p.start, p.end], last_p.level + 1, p.type); } } } else { if (p.level !== 0) { setLevel(cm, [p.start, p.end], 0, p.type); } } } last_p = p; // we can process content block instead of line if (p) { i += (p.end - p.start); } } function findLastAtLevel(_cm, line, line_limit_inf, level) { let p; do { line -= 1; if ((p = isNumberedList(_cm, line)) && p.level === level) { return p; } } while (line > line_limit_inf); return null; } function setLevel(_cm, range, level, type) { let content; let i; for (i=range[0]; i<=range[1]; i++) { content = cm.getLine(i).trimLeft(); const n_spaces = function(_level, _line, _type) { let spaces = _level * 3; if (_line > 0) { spaces += _type === "numbered" ? 3 : 2; } return spaces; }(level, i - range[0], type); content = " ".repeat(n_spaces) + content; cm.replaceRange( content, { line: i, ch: 0 }, { line: i, ch: _cm.getLine(i).length }, ); } } function setNumber(_cm, line, level) { const content = _cm.getLine(line); const new_content = content.replace(/[0-9]+\./, level+"."); cm.replaceRange( new_content, { line: line, ch: 0 }, { line: line, ch: content.length }, ); } function find_limit_inferior(_cm, _line) { let content; let p; let match; let line_candidate = _line; do { content = _cm.getLine(_line); p = isNumberedList(_cm, _line); match = /(\s+).*$/.exec(content); if (p) line_candidate = _line; if (!p || !match) break; _line -= 1; } while (_line >= 0); return line_candidate; } function find_limit_superior(_cm, _line) { let content; let p; let match; let line_candidate = _line; do { content = _cm.getLine(_line); p = isNumberedList(_cm, _line); match = /(\s+).*$/.exec(content); if (p) line_candidate = _line; if (!p || !match) break; _line += 1; } while (_line < _cm.lineCount()); return line_candidate; } } function swap(cm, from, to) { const from_content = cm.getRange( { line: from[0], ch: 0 }, { line: from[1], ch: cm.getLine(from[1]).length }, ); const to_content = cm.getRange( { line: to[0], ch: 0 }, { line: to[1], ch: cm.getLine(to[1]).length }, ); const cursor = cm.getCursor(); if (to[0] > from[0]) { // moving down cm.replaceRange( from_content, { line: to[0], ch: 0 }, { line: to[1], ch: cm.getLine(to[1]).length }, ); cm.replaceRange( to_content, { line: from[0], ch: 0 }, { line: from[1], ch: cm.getLine(from[1]).length }, ); cm.setCursor({ line: cursor.line + (to[1] - to[0] + 1), ch: cursor.ch, }); } else { // moving up cm.replaceRange( to_content, { line: from[0], ch: 0 }, { line: from[1], ch: cm.getLine(from[1]).length }, ); cm.replaceRange( from_content, { line: to[0], ch: 0 }, { line: to[1], ch: cm.getLine(to[1]).length }, ); cm.setCursor({ line: cursor.line - (to[1] - to[0] + 1), ch: cursor.ch, }); } } export function fold(cm, start) { cm.foldCode(start, null, "fold"); } export function unfold(cm, start) { cm.foldCode(start, null, "unfold"); } export 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; }