imdone-core
Version:
1,514 lines (1,352 loc) • 38.7 kB
JavaScript
import _isFunction from 'lodash.isfunction'
import _isUndefined from 'lodash.isundefined'
import _isString from 'lodash.isstring'
import { omit } from './omit.js'
import _reject from 'lodash.reject'
import _noop from 'lodash.noop'
import _some from 'lodash.some'
import _remove from 'lodash.remove'
import _groupBy from 'lodash.groupby'
import _where from 'lodash.where'
import _union from 'lodash.union'
import Emitter from 'events'
import { languages } from './languages.js'
import util from 'util'
import { parallel, eachLimit, eachSeries, series } from 'async'
import path from 'path'
import ignore from 'ignore'
import { File } from './file.js'
import eol from 'eol'
import { tools } from './tools.js'
const { inMixinsNoop } = tools
import { constants } from './constants.js'
import debug from 'debug'
const log = debug('imdone-core:Repository')
import { List } from './list.js'
import monquery from 'monquery'
import sift from 'sift'
import { sort } from 'fast-sort'
import JSONfns from 'json-fns'
import { Task } from './task.js'
import { newCard } from './card.js'
import { replaceDateLanguage } from './adapters/parsers/DateLanguageParser.js'
import { getRawTask, isNumber, LIST_NAME_PATTERN } from './adapters/parsers/task/CardContentParser.js'
import { interpolate } from './adapters/parsers/content-transformer.js'
import XRegExp from 'xregexp'
import { appContext } from './context/ApplicationContext.js'
import { computeChecksum } from './checksum.js'
import { getTasksToModify } from './get-tasks-to-modify.js'
import { logger } from './adapters/logger.js'
const { ERRORS, ASYNC_LIMIT, DEFAULT_FILE_PATTERN} = constants
const DEFAULT_SORT = [{ asc: u => isNumber(u.order) ? u.order : Infinity }, { asc: u => u.text }]
function getTasksByList (
repo,
tasksAry,
noSort,
populateFiltered
) {
if (!repo) return []
var tasks = {}
var allTasks = noSort ? tasksAry : sort(tasksAry).by(DEFAULT_SORT)
allTasks.forEach(function (task) {
if (!tasks[task.list]) tasks[task.list] = []
tasks[task.list].push(task)
})
var lists = repo.getLists()
lists.forEach((list) => {
if (list.filter && populateFiltered)
return Repository.populateFilteredList(list, tasksAry)
list.tasks = tasks[list.name] || []
})
return lists
}
function populateFilteredList (list, tasks) {
try {
list.tasks = Repository.query(tasks, list.filter).map((task) => {
task.filteredListName = list.name
return task
})
} catch (e) {
list.tasks = []
}
}
function regexQuery (tasks, queryString) {
return tasks
.map((task) => {
const escapedQueryString = tools.escapeRegExp(queryString.trim());
// Regex to match occurrences not part of a URL and not already surrounded by `==`
const regex = new RegExp(
`(?<!https?:\\/\\/[^\\s]*?)\\b${escapedQueryString}\\b(?![^\\s]*\\/)(?<!==)(?!==)`,
'gi'
);
// Regex to detect Markdown links `[title](url)` where `queryString` is in title or URL
const markdownLinkRegex = new RegExp(
`\\[([^\\]]*${escapedQueryString}[^\\]]*)\\]\\(([^)]+${escapedQueryString}[^)]*)\\)`,
'gi'
);
let match = task.content.match(regex) || task.content.match(markdownLinkRegex);
task.match = match;
if (match && queryString.trim() && task.interpretedContent) {
// Replace standalone occurrences
task.interpretedContent = task.interpretedContent.replace(
regex,
'==$&=='
);
// Replace Markdown links where queryString is in title or URL
task.interpretedContent = task.interpretedContent.replace(
markdownLinkRegex,
(match) => `==${match}==`
);
task.updateDescriptionData();
}
return task;
})
.filter(({ match }) => match);
};
function parseHideListsFromQueryString (queryString) {
const hideLists = []
const regex = `hide:(\\s*((${LIST_NAME_PATTERN},?\\s?)+))?`
queryString = queryString.replaceAll(
// hide lists example `hide: list1, list2`
new XRegExp(regex, 'g'),
(match, allLists, lists) => {
lists && lists.split(',').forEach((list) => {
hideLists.push(list.trim())
})
return ''
}
).trim()
return {
hideLists,
queryString,
}
}
function parseSortFromQueryString (queryString) {
const sort = []
queryString = queryString.replace(
/\s([+-])([A-Za-z.]+)/g,
(match, order, attr) => {
const direction = order === '+' ? 'asc' : 'desc'
const sortString = `{ "${direction}": "function (o) { return o.${attr};}" }`
sort.push(JSONfns.parse(sortString))
return ''
}
)
return {
sort,
queryString,
}
}
function parseSortFromMongoQuery (mongoQuery) {
const sort = []
for (const [key, value] of Object.entries(mongoQuery.sort)) {
if (value > 0) sort.push({ asc: key })
else sort.push({ desc: key })
}
return sort
}
// function sortByQuery (tasks, queryString = '') {
// queryString = replaceDateLanguage(queryString)
// let { sort } = Repository.parseSortFromQueryString(queryString)
// if (!sort || sort.length === 0) sort = DEFAULT_SORT
// sort(tasks).by(sort)
// }
function filterCards (tasks, _queryString = '') {
let query
_queryString = replaceDateLanguage(_queryString)
let { sort: userSort, queryString } = Repository.parseSortFromQueryString(_queryString);
const hideListsQuery = Repository.parseHideListsFromQueryString(queryString);
queryString = hideListsQuery.queryString;
const hideLists = hideListsQuery.hideLists;
try {
query = monquery(queryString)
} catch (e) {
log(`Unable to parse ${_queryString} using monquery`)
}
let result = []
if (query) result = tasks.filter(sift.default(query))
if (!query || result.length === 0) {
result = Repository.regexQuery(tasks, queryString)
}
if (!userSort || userSort.length === 0) userSort = DEFAULT_SORT
const sorted = sort(result).by(userSort)
return {
result: sorted,
query,
sort: userSort,
hideLists
}
}
function query (tasks, queryString) {
return Repository.filterCards(tasks, queryString).result
}
function replaceDatesInQuery (query) {
return Repository.filterObjectValues(query, (key, value) => {
let date = Date.parse(value)
if (date && /(\d{4})-(\d{2})-(\d{2})/.test(value)) {
return date
}
return value
})
}
function filterObjectValues (o, cb) {
if (o && typeof o === 'object') {
for (const [key, value] of Object.entries(o)) {
if (value && typeof value === 'object') {
Repository.filterObjectValues(value, cb)
} else {
o[key] = cb(key, value)
}
}
}
return o
}
// Emits task.found, list.found, file.update and file.delete, file.processed, files.saved
export class Repository extends Emitter {
constructor(_path, config) {
super()
this.config = config
this.path = _path
this.files = []
this.languages = languages
this.allMeta = {}
this.metaKeys = new Set()
this.allTags = new Set()
this.allTopics = new Set()
this.allContexts = new Set()
}
static getTasksByList = getTasksByList
static populateFilteredList = populateFilteredList
static regexQuery = regexQuery
static parseHideListsFromQueryString = parseHideListsFromQueryString
static parseSortFromQueryString = parseSortFromQueryString
static parseSortFromMongoQuery = parseSortFromMongoQuery
// static sortByQuery = sortByQuery
static filterCards = filterCards
static query = query
static replaceDatesInQuery = replaceDatesInQuery
static filterObjectValues = filterObjectValues
init () {
inMixinsNoop()
}
// READY Refactor to use async/await
// #esm-migration #urgent #important
// <!--
// order:-240
// -->
async refresh () {
this.files = []
this.allMeta = {}
this.metaKeys = new Set()
this.allTags = new Set()
this.allTopics = new Set()
this.allContexts = new Set()
this.config.dirty = true
let files = await this.getFilesInPath(false)
try {
const config = await this.loadConfig()
this.config = config
files = await this.readFiles()
this.emit('initialized', { ok: true, lists: this.getTasksByList() })
} catch (err) {
this.emit('initialized', { ok: false })
throw err
}
return files
}
/**
* Description
* @method destroy
* @return
*/
async destroy () {
this.destroyed = true
this.removeAllListeners()
}
/**
* Description
* @method getId
* @return CallExpression
*/
getId () {
return this.getPath()
}
getProject () {
return this.project
}
getDisplayName () {
return path.basename(this.path)
}
emitFileUpdate (file, force) {
if (force || this.shouldEmitFileUpdate(file)) this.emit('file.update', file)
}
shouldEmitFileUpdate (file) {
if (this.moving) return
if (this.lastMovedFiles) {
var index = this.lastMovedFiles.indexOf(file)
if (index > -1) {
this.lastMovedFiles.splice(index, 1)
} else {
if (file && file.updated) return true
}
} else {
if (file && file.updated) return true
}
}
emitConfigUpdate (data) {
if (this.savingConfig) return
process.nextTick(() => {
this.emit('config.update', data)
})
}
// READY Refactor createListeners to use async/await
// #esm-migration
// <!--
// order:-120
// -->
createListeners () {
if (this.taskListener) return
this.taskListener = async (event, task) => {
if (!this.listExists(task.list) && this.config.includeList(task.list)) {
const list = new List({ name: task.list })
await this.addList(list)
this.emit('list.found', list)
await this.saveConfig()
}
Object.keys(task.allMeta).forEach(key => this.metaKeys.add(key))
task.topics.forEach(topic => this.allTopics.add(topic))
task.allTags.forEach(tag => this.allTags.add(tag))
task.allContext.forEach(context => this.allContexts.add(context))
this.allMeta = this.addAllMeta(task.allMeta)
this.emit(event, task)
}
this.taskFoundListener = (task) => {
this.taskListener('task.found', task)
}
this.taskModifiedListener = (task) => {
this.taskListener('task.modified', task)
}
}
addAllMeta (meta) {
const allMeta = Object.assign({}, this.allMeta)
Object.keys(meta).forEach((key) => {
if (!allMeta[key]) {
allMeta[key] = meta[key]
return
}
allMeta[key] = _union(allMeta[key], meta[key])
})
return allMeta
}
/**
* Description
* @method addList
* @param {} list
* @return
*/
async addList (list) {
if (this.listExists(list.name)) return;
list = omit(list, 'tasks');
this.config.lists.push(new List(list));
if (!list.filter && !/[a-z]/.test(list.name)) {
const codeList = list.name.replace(/\s+/g, '-').toUpperCase();
if (!this.config.code.include_lists.find((name) => name === codeList)) {
this.config.code.include_lists.push(codeList);
}
}
await this.saveConfig()
this.emit('list.modified', list);
};
// READY Refactor removeList to use async/await
// #esm-migration
// <!--
// order:-220
// -->
async removeList (list) {
if (!this.listExists(list)) return
var lists = _reject(this.getLists(), { name: list })
if (this.config.code && this.config.code.include_lists) {
this.config.code.include_lists = _reject(this.config.code.include_lists, list)
}
this.setLists(lists)
await this.saveConfig()
this.emit('list.modified', list)
}
/**
* Description
* @method getPath
* @return MemberExpression
*/
getPath () {
return this.path
}
/**
* Description
* @method getConfig
* @return MemberExpression
*/
getConfig () {
return this.config
}
/**
* Description
* @method getLists
* @return MemberExpression
*/
getLists () {
const config = this.getConfig()
return config ? config.lists.map(obj => ({...obj})) : []
}
getVisibleLists () {
return _reject(this.getLists(), 'hidden')
}
isListVisible (name) {
return this.getVisibleLists.find((list) => list.name === name)
}
/**
* Description
* @method setLists
* @param {} lists
* @return ThisExpression
*/
setLists (lists) {
this.config.lists = lists.map((list) => {
return new List(list)
})
return this
}
/**
* Description
* @method listExists
* @param {} name
* @return BinaryExpression
*/
listExists (name) {
return this.getConfig().listExists(name)
}
/**
* Save the config file (Implemented in mixins)
*
* @method saveConfig
* @return
*/
async saveConfig () {
inMixinsNoop()
}
/**
* Load the config file (Implemented in mixins)
*
* @method loadConfig
* @return MemberExpression
*/
// READY Refactor loadConfig to use async/await
// #esm-migration #urgent
// <!--
// order:-160
// -->
async loadConfig () {
inMixinsNoop()
}
async migrateTasksByConfig (oldConfig, newConfig) {
return new Promise((resolve, reject) => {
if (!oldConfig || !newConfig) return resolve()
const oldMetaSep = oldConfig.getMetaSep()
const newMetaSep = newConfig.getMetaSep()
if (oldMetaSep === newMetaSep) return resolve()
eachLimit(
this.getFiles(),
ASYNC_LIMIT,
(file, cb) => {
const tasks = sort(file.tasks).desc(u => u.line)
eachSeries(
tasks,
async (task, cb) => {
if (!Task.isTask(task)) return cb()
try {
task.replaceMetaSep(oldMetaSep, newMetaSep)
await this.modifyTask(task, false)
cb()
} catch (err) {
cb(err)
}
},
async (err) => {
if (err) return cb(err)
if (!file.isModified() || file.getContent().trim() === '') return cb()
try {
const file = await this.writeFile(file)
this.resetFile(file)
cb(null, file)
} catch (err) {
cb(err)
}
}
)
},
(err) => {
if (err) return reject(err)
resolve()
}
)
})
}
/**
* Get the full path from a relative path
*
* @method getFullPath
* @param {} file
* @return String
*/
getFullPath (file) {
if (File.isFile(file)) file = file.path
if (file.indexOf(this.path) === 0) return file
try {
var fullPath = path.join(this.path, file)
return fullPath
} catch (e) {
throw new Error(
util.format(
'Error getting full path for file:%s and repo path:%s',
file,
this.path
)
)
}
}
/**
* Get the relative path from repository root
*
* @method getRelativePath
* @param {} fullPath
* @return String
*/
getRelativePath (fullPath) {
if (path.sep === '\\') {
fullPath = fullPath.replace(/\//g, path.sep)
}
if (!fullPath.startsWith(this.path)) return fullPath
try {
var relPath = path.relative(this.path, fullPath)
return relPath
} catch (e) {
throw new Error(
util.format(
'Error getting relative path for file:%s and repo path:%s',
fullPath,
this.path
)
)
}
}
/**
* Is this file OK? Implemented in mixins
*
* @method fileOK
* @param {} file
* @param {} includeDirs
* @return stat
*/
// READY Refactor fileOK to use async/await
// #esm-migration #urgent
async fileOK (file, includeDirs) {
inMixinsNoop()
}
setIgnores (ignores) {
// logger.info('ignore patterns:', ignores)
this.ignorePatterns = ignores
this.ignore = ignore().add(ignores)
}
/**
* Should the relative path be included.
*
* @method shouldInclude
* @param {} relPath
* @return exclude
*/
shouldInclude (relPath) {
relPath = this.getRelativePath(relPath)
let include = true
if (this.ignore) {
try {
include = relPath && !this.ignore.ignores(relPath)
if (this.config.markdownOnly && include && relPath) {
include = File.isMarkdown(relPath)
}
} catch (e) {
logger.error(
`Failed to check ignore status for dir: [${relPath}] in: [${this.path}]. It will be included.`,
e
)
}
}
return include
}
/**
* Add or replace a file in the files reference array
*
* @method addFile
* @param {} file
* @return MemberExpression
*/
// READY Refactor addFile to use async/await
// #esm-migration #urgent
async addFile (file) {
if (this.destroyed) throw new Error('destroyed')
const ok = await this.fileOK(file)
if (!ok) return this.files
let index = this.files.findIndex(({ path }) => path === file.path)
if (index > -1) {
this.files[index] = file
} else {
this.files.push(file)
}
return this.files
}
/**
* Remove a file from the files reference array
*
* @method removeFile
* @param {} file
* @return MemberExpression
*/
removeFile (file) {
if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
_remove(this.files, f => f.path === file.path)
return this.files
}
/**
* Description
* @method getFile
* @param {} path
* @return CallExpression
*/
getFile (path) {
path = this.getRelativePath(path)
return this.files.find((file) => file.path === path)
}
getTask (id) {
if (Task.isTask(id)) {
let task = id
if (!task.meta.id) return
return this.getTasks().find((existingTask) => {
return existingTask.meta.id && existingTask.meta.id[0] == task.meta.id[0]
})
} else {
let task = this.getTasks().find((task) => task.id === id)
if (!task) return
return this.getFile(task.source.path)
.getTasks()
.find((task) => task.id === id)
}
}
/**
* Description
* @method getFileForTask
* @param {} task
* @return CallExpression
*/
getFileForTask (task) {
return this.getFile(task.getSource().path)
}
/**
* Descriptione
* @method getFiles
* @param {} paths
* @return CallExpression
*/
getFiles (paths) {
if (_isUndefined(paths)) return sort(this.files).asc(u => u.path)
return this.files.filter((file) => {
return paths.includes(file.path)
})
}
getFilesWithTasks () {
const files = this.files.filter((file) => file.getTasks().length > 0)
return sort(files).asc(u => u.path)
}
resetFile (file) {
file.reset()
file.removeListener('task.found', this.taskFoundListener)
file.removeListener('task.modified', this.taskModifiedListener)
}
// READY refactor extractTasks to use async/await
// #esm-migration #urgent #important
// <!--
// order:-100
// -->
async extractTasks (file) {
if (file.content === null) {
await this.readFileContent(file)
}
file.on('task.found', this.taskFoundListener)
file.on('task.modified', this.taskModifiedListener)
const fileContent = file.content
file.extractAndTransformTasks(this.getConfig())
if (!file.isModified() || fileContent === file.content) {
this.resetFile(file)
return file
}
file.extractTasks(this.getConfig())
if (file.modified) await this.writeFile(file)
this.resetFile(file)
return file
}
/**
* Implemented in mixins
* @method writeFile
* @param {} file
* @param {} cb
* @return
*/
async writeFile (file) {
inMixinsNoop()
}
/**
* Implemented in mixins
* @method getFilesInPath
* @param {} includeDirs
* @return CallExpression
*/
async getFilesInPath (includeDirs) {
inMixinsNoop()
}
/**
* Implemented in mixins
* @method readFileContent
* @param {} file
* @return
*/
async readFileContent (file) {
inMixinsNoop()
}
// READY Refactor readFile to use async/await
// #esm-migration #important #urgent
// <!--
// order:-30
// -->
async readFile(file) {
if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
if (file.deleted) return file
var currentChecksum = file.checksum
const filePath = file.path
if (/\.\.(\/|\\)/.test(filePath)) throw new Error('Unable to read file:' + file)
await this.readFileContent(file)
file.checksum = computeChecksum(file.getContent())
file.updated = currentChecksum !== file.checksum
if (!file.updated) return file
await this.extractTasks(file)
await this.addFile()
return file
}
/**
* Description
* @method readFiles
* @param {} files
* @param {} cb
* @return
*/
// READY Refactor readFiles to use async/await
// #esm-migration #urgent #important
// <!--
// order:-10
// -->
readFiles (files = this.files) {
this.allMeta = {}
this.metaKeys = new Set()
this.allTags = new Set()
this.allContexts = new Set()
return new Promise(async (resolve, reject) => {
if (files.length < 1) {
files = this.files = await this.getFilesInPath(false)
const filesToInclude = files.map((file) => file.path)
const fileStats = {
count: filesToInclude.length,
files: filesToInclude,
}
this.emit('files.found', fileStats)
}
let completed = 0
if (files.length < 1) return resolve(files)
eachLimit(
files,
ASYNC_LIMIT,
async (file) => {
this.emit('file.reading', { path: file.path })
await this.readFile(file)
completed++
this.emit('file.read', {path: file.path, completed: completed})
},
function (err) {
if (err) return reject(err)
resolve(files)
}
)
})
}
/**
* Implemented in mixins
* @method deleteFile
* @param {} path
* @param {} cb
* @return
*/
async deleteFile (path, cb) {
inMixinsNoop(cb)
}
/**
* Description
* @method hasDefaultFile
* @return CallExpression
*/
hasDefaultFile () {
return _some(this.getFiles(), function (file) {
var regex = new RegExp(DEFAULT_FILE_PATTERN, 'i')
return regex.test(file.path)
})
}
/**
* Description
* @method getDefaultFile
* @return file
*/
getDefaultFile () {
var files = sort(this.getFiles()).asc(u => u.path)
var file = files.reverse().find((file) => {
var regex = new RegExp(DEFAULT_FILE_PATTERN, 'i')
return regex.test(file.path)
})
return file
}
getList (name) {
return this.getLists().find((list) => list.name === name)
}
getListById(id, lists = this.getLists()) {
return lists.find((list) => list.id === id)
}
// READY Refactor hideList to use async/await
// #esm-migration
// <!--
// order:-150
// -->
async hideList (name) {
await this.setListHidden(name)
}
// READY Refactor showList to use async/await
// #esm-migration
// <!--
// order:-230
// -->
async showList (name) {
await this.setListHidden(name, false)
}
async setListHidden (name, hidden = true) {
const list = this.getList(name)
if (list) {
list.hidden = hidden
await this.saveConfig()
this.emit('list.modified', name)
}
}
/**
* Description
* @method moveList
* @param {} name
* @param {} pos
* @param {} cb
* @return
*/
// READY Refactor moveList to use async/await
// #esm-migration
// <!--
// order:-190
// -->
async moveList (name, pos) {
var list = this.getList(name)
if (list) {
_remove(this.getLists(), { name: name })
this.getLists().splice(pos, 0, list)
await this.saveConfig()
this.emit('list.modified', name)
}
}
// READY Refactor toggleListIgnore to use async/await
// #esm-migration
// <!--
// order:-250
// -->
async toggleListIgnore (name) {
var list = this.getList(name)
if (!list) return reject(new Error('List not found'))
list.ignore = !list.ignore
await this.updateList(list.id, list)
await this.saveConfig()
this.emit('list.modified', name)
}
async toggleList (name) {
const list = this.getList(name)
if (!list) throw new Error('List not found')
list.hidden = !list.hidden
await this.updateList(list.id, list)
await this.saveConfig()
this.emit('list.modified', name)
}
// READY Refactor updateList to use async/await
// #esm-migration
// <!--
// order:-90
// -->
async updateList(id, {name, hidden, ignore, filter}) {
const lists = this.getLists()
const list = this.getListById(id, lists)
const oldName = list.name
const hasFilter = list.filter
list.name = name
list.hidden = hidden
list.ignore = ignore
if (hasFilter !== undefined) list.filter = filter
this.setLists(lists)
if (oldName === name || hasFilter) {
return await this.saveConfig()
}
await this.moveTasksBetweenLists(oldName, name)
}
getTasksByFile(tasks) {
const tasksByFile = {} // path: [files]
tasks.forEach((task) => {
const filePath = task.path
if (!tasksByFile[filePath]) {
tasksByFile[filePath] = { file: this.getFileForTask(task), tasks: [] }
}
tasksByFile[filePath].tasks.push(task)
tasksByFile[filePath].tasks = sort(tasksByFile[filePath].tasks).desc(u => u.line)
})
return tasksByFile
}
// READY Refactor moveTasksBetweenLists to use async/await
// <!--
// #esm-migration #needs-testing
// order:-80
// -->
// ## Tasks
// - [x] Refactor
// - [ ] Test
async moveTasksBetweenLists(oldName, newName) {
return new Promise((resolve, reject) => {
const tasksToModify = this.getTasksInList(oldName)
const tasksByFile = this.getTasksByFile(tasksToModify) // { <path/to/file>: {file, tasks} }
const modifyTasksFunctions = Object.values(tasksByFile).map(({ file, tasks }) => {
return async () => {
await this.readFileContent(file)
tasks.forEach((task) => {
task.list = newName
file.modifyTask(task, this.getConfig(), true)
file.modified = true
})
return file
}
})
if (modifyTasksFunctions.length < 1) return resolve()
this.moving = true
parallel(modifyTasksFunctions, async (err, files) => {
this.moving = false
if (err) {
files.forEach(file => this.resetFile(file))
return reject(err)
}
try {
await this.saveConfig()
await this.saveModifiedFiles()
resolve(files)
} catch (err) {
reject(err)
}
})
})
}
// READY Migrate writeAndExtract to use async/await
// #esm-migration
// <!--
// order:-80
// -->
async writeAndExtract (file, emit) {
try {
await this.writeFile(file, emit)
} catch (err) {
new Error('Unable to write file:' + file.path, {cause: err})
}
try {
await this.extractTasks(file)
} catch (err) {
new Error('Unable to extract tasks for file:' + file.path, { cause: err })
}
try {
await this.addFile(file)
} catch (err) {
new Error('Unable to add file after extracting tasks: ' + file.path, { cause: err })
}
return file
}
// READY Refactor writeAndAdd to use async/await
// #esm-migration
// <!--
// order:-70
// -->
async writeAndAdd (file, emit) {
await this.writeFile(file, emit)
await this.addFile(file)
return file
}
// READY Refactor deleteTask to use async/await
// #esm-migration
// <!--
// order:-130
// -->
async deleteTask (task, cb) {
let file = this.getFileForTask(task)
if (!file) return
if (!file.getContent()) {
file = await this.readFileContent(file)
}
file.deleteTask(task, this.getConfig())
if (file.getContentForFile().trim() === '' && file.isMarkDownFile()) {
if (file.deleted) return
logger.log('Deleting empty file:', file.path)
this.deleteFile(file.path)
file.deleted = true
return
}
await this.writeAndExtract(file, true)
}
// READY Refactor deleteTasks to use async/await
// #esm-migration
// <!--
// order:-140
// -->
async deleteTasks(tasks) {
const files = {}
tasks.forEach((task) => {
if (!files[task.source.path]) {
files[task.source.path] = []
}
files[task.source.path].push(task)
files[task.source.path] = sort(files[task.source.path]).desc(u => u.line)
})
await Promise.all(
Object.keys(files).map(async (path) => {
const tasksInFile = files[path]
if (!tasksInFile) return
for (const task of tasksInFile) {
const taskToDelete = newCard(task, this.project, true)
await this.deleteTask(taskToDelete)
}
})
)
this.emit('tasks.updated', tasks)
}
// READY Refactor modifyTaskFromHtml to use async/await
// #esm-migration
// <!--
// order:-170
// -->
async modifyTaskFromHtml (task, html) {
var file = this.getFileForTask(task)
if (!file.getContent()) {
file = await this.readFileContent(file)
}
file.modifyTaskFromHtml(task, html)
return await this.writeAndExtract(file, true)
}
// READY Refactor modifyTaskFromContent to use async/await
// #esm-migration
// <!--
// order:-180
// -->
async modifyTaskFromContent (task, content, cb) {
var file = this.getFileForTask(task)
if (!file.getContent()) {
file = await this.readFileContent(file)
}
file.modifyTaskFromContent(task, content, this.getConfig())
return await this.writeAndExtract(file, true)
}
getTaskContent({
description,
order,
text,
taskPrefix,
taskSyntax,
}) {
const task = newCard(
{
description,
meta: {},
order,
text,
beforeText: taskPrefix,
type: taskSyntax,
},
this.project
)
task.updateOrderMeta(this.config)
return task.description.join(eol.lf)
}
// READY Refactor appendTask to use async/await
// #esm-migration
// <!--
// order:-300
// -->
async appendTask({file, content, list}) {
const config = this.getConfig()
const interpretedTaskPrefix = interpolate(
config.getTaskPrefix(),
{ date: new Date() }
).content.trimEnd()
const lines = eol.split(content)
const text = lines[0]
const taskSyntax = config.getNewCardSyntax()
const taskPrefix = interpretedTaskPrefix ? `${interpretedTaskPrefix} ` : ''
let fileContent = file.getContent()
const journalTemplate = this.getConfig().journalTemplate
fileContent = fileContent.trim()
? fileContent
: journalTemplate
&& journalTemplate.trim()
? journalTemplate + String(eol.lf)
: '';
const length = fileContent.length
const fileIsEmpty = length < 1
const crlf = String(eol.crlf)
let sep = fileContent.indexOf(crlf) > -1 ? crlf : String(eol.lf)
if (fileContent.endsWith(sep) || fileIsEmpty) sep = ''
const order = appContext().projectContext.getOrder(list)
const description = lines.length > 1 ? Task.padDescription(lines.slice(1), taskPrefix) : []
const { orderMeta, tokenPrefix } = config
const rawTask = getRawTask({tokenPrefix, orderMeta, list, order, text, type: taskSyntax})
const taskContent = config.orderMeta
? this.getTaskContent({
description,
order,
text,
taskPrefix,
taskSyntax,
})
: description.join(eol.lf)
let appendContent = File.trimBlankLines(
`${taskPrefix}${rawTask}${eol.lf}${taskContent}`
)
fileContent = `${fileContent}${sep}`
const cardTerminator = "\n".repeat(2)
file.setContent(`${fileContent}${appendContent}${cardTerminator}`)
await this.writeAndExtract(file)
const task = file.getTasks().find(task => task.text === text && task.list === list)
return { file, task }
}
// READY Refactor addTaskToFile to use async/await
// #esm-migration
// <!--
// order:-290
// -->
async addTaskToFile (filePath, list, content) {
const relPath = this.getRelativePath(filePath)
let file = this.getFile(relPath)
if (file) {
await this.readFileContent(file)
return await this.appendTask({file, content, list})
} else {
const modifiedTime = new Date()
const createdTime = new Date()
file = new File({
repoId: this.path,
filePath: relPath,
content: '',
modifiedTime,
createdTime,
project: this.project,
})
return await this.appendTask({file, content, list})
}
}
/**
* Description
* @method modifyTask
* @param {} task
* @return CallExpression
*/
// READY Refactor modifyTask to use async/await
// #esm-migration #important #urgent
// <!--
// order:-60
// -->
async modifyTask (task, writeFile = false) {
if (!Task.isTask(task)) return
const config = this.getConfig()
log(
'Modifying Task... text:%s list:%s order:%d path:%s id:%s line:%d',
task.text,
task.list,
task.order,
task.source.path,
task.id,
task.line
)
let beforeModifyContent
const file = this.getFileForTask(task)
try {
beforeModifyContent = file.getContent()
} catch (e) {
logger.error(`Can't get file for task: {text:'${task.text}', path:'${task.source.path}', line:${task.line}}`)
throw e
}
if (!beforeModifyContent) await this.readFileContent(file)
file.modifyTask(task, config, true)
file.extractTasks(config)
file.transformTask({config, modify:true, task})
file.extractTasks(config)
if (!writeFile || beforeModifyContent === file.getContent()) return task
return await this.writeAndAdd(file)
}
// READY Refactor moveTask to use async/await
// #esm-migration
// <!--
// order:-200
// -->
async moveTask ({ task, newList, newPos }) {
if (!Task.isTask(task)) {
task = newCard(task, this.project, true)
}
task = this.getTask(task.id)
var toListTasks = this.getTasksInList(newList)
if (toListTasks === undefined)
throw new Error(ERRORS.LIST_NOT_FOUND, newList)
var fromListTasks = this.getTasksInList(task.list)
if (fromListTasks === undefined)
throw new Error(ERRORS.LIST_NOT_FOUND, task.list)
var sameList = newList == task.list
if (!sameList) task.oldList = task.list
task.list = newList
// Move the task to the correct position in the list
fromListTasks = _reject(fromListTasks, function (_task) {
return _task.equals(task)
})
toListTasks = _reject(toListTasks, function (_task) {
return _task.equals(task)
})
const tasksToModify = getTasksToModify(task, toListTasks, newPos)
task.updateOrderMeta(this.config)
toListTasks.splice(newPos, 0, task)
const tasksToModifySorted = sort(tasksToModify).by([{ desc: u => u.line }, { asc: u => u.path }])
await eachSeries(
tasksToModifySorted,
async (task) => {
await this.modifyTask(task, true)
},
)
return task
}
// READY Refactor moveTasks to use async/await
// #esm-migration #important
// <!--
// order:-350
// -->
async moveTasks (tasks, newList, newPos = 0, noEmit = false) {
const log = debug('moveTasks')
if (this.getList(newList).filter)
throw new Error(`Tasks can\'t be moved to a filtered list ${newList}.`)
const listsModified = [newList]
this.moving = true
log('Move tasks to list:%s at position:%d : %j', newList, newPos, tasks)
log(
'newList before mods:',
JSON.stringify(this.getTasksInList(newList), null, 3)
)
await series(
sort(tasks).by({desc: t => t.line}).map((task, i) => {
return async () =>{
const foundTask = this.getTasks().find(({source, line}) => task.source.path === source.path && task.line === line)
if (foundTask) {
if (listsModified.indexOf(foundTask.list) < 0) listsModified.push(foundTask.list)
await this.moveTask({ task: foundTask, newList, newPos: newPos + i, noEmit: true })
}
}
})
)
const results = await this.saveModifiedFiles()
this.lastMovedFiles = results
this.moving = false
if (!noEmit) this.emit('tasks.moved', tasks)
return listsModified.map((list) => {
return {
list: list,
tasks: this.getTasksInList(list),
}
})
}
getModifiedFiles () {
var filesToSave = []
this.getFiles().forEach((file) => {
if (file.isModified()) filesToSave.push(file)
})
return filesToSave
}
// READY Refactor saveModifiedFiles to use async/await
// #esm-migration #urgent #important
// <!--
// order:-110
// -->
async saveModifiedFiles () {
var filesToSave = this.getModifiedFiles()
var funcs = filesToSave.map((file) => {
return async () => {
file.checksum = computeChecksum(file.getContent())
await this.writeAndExtract(file, false)
}
})
if (funcs.length < 1) return
this.savingFiles = true
await Promise.all(funcs)
this.savingFiles = false
this.emit('files.saved', filesToSave)
}
/**
* Description
* @method getTasks
* @return tasks
*/
getTasks () {
const tasks = []
this.getFiles().forEach((file) => {
Array.prototype.push.apply(tasks, file.getTasks())
})
return tasks
}
getTasksByList (noSort) {
return Repository.getTasksByList(this, this.getTasks(), noSort, true)
}
/**
* Description
* @method getTasksInList
* @param {} name
* @return ConditionalExpression
*/
getTasksInList (name, offset, limit) {
if (!_isString(name)) return []
var tasks = _where(this.getTasks(), { list: name })
if (tasks.length === 0) return []
var allTasks = sort(tasks).by(DEFAULT_SORT)
if (isNumber(offset) && isNumber(limit))
return allTasks.slice(offset, offset + limit)
return allTasks
}
getTaskIndex (task) {
const tasks = this.getTasksInList(task.list)
let index = 0
tasks.forEach((t, i) => {
if (t.id === task.id) index = i
})
return index
}
query (queryString) {
const result = Repository.query(this.getTasks(), queryString)
return Repository.getTasksByList(this, result, true)
}
}