Files
2022-01-30 23:38:40 +11:00

729 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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