Comparing version 0.0.38 to 0.0.39
@@ -7,3 +7,3 @@ "use strict"; | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments)).next()); | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
@@ -10,0 +10,0 @@ }; |
@@ -7,3 +7,3 @@ "use strict"; | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments)).next()); | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
@@ -10,0 +10,0 @@ }; |
"use strict"; | ||
const assert = require("assert"); | ||
const workspace_1 = require("./workspace"); | ||
const stringify_1 = require("../util/stringify"); | ||
const otTestCases_1 = require("./otTestCases"); | ||
function orderedStringify(obj) { | ||
return JSON.stringify(obj, Object.keys(obj).sort()); | ||
} | ||
describe("noop", () => { | ||
@@ -15,6 +19,6 @@ [ | ||
].forEach(({ op, want }) => { | ||
it(`${JSON.stringify(op)}`, () => assert.equal(workspace_1.noop(op), want)); | ||
it(stringify_1.stringify `${op}`, () => assert.equal(workspace_1.noop(op), want)); | ||
}); | ||
}); | ||
describe("compose", () => { | ||
describe("compose:", () => { | ||
const tests = otTestCases_1.composeTests; | ||
@@ -25,6 +29,7 @@ const testCompose = (title, t) => { | ||
if (wantErr) { | ||
assert.throws(() => workspace_1.compose(a, b)); | ||
assert.throws(() => workspace_1.compose(a, b), stringify_1.stringify `compose:\n a: ${a}\n b: ${b}`); | ||
} | ||
else { | ||
assert.deepEqual(workspace_1.compose(a, b), want); | ||
let got = workspace_1.compose(a, b); | ||
assert.deepEqual(got, want, stringify_1.stringify `compose:\n a: ${a}\n b: ${b}\n got: ${got}\n want: ${want}`); | ||
} | ||
@@ -36,17 +41,2 @@ }); | ||
const opTypes = new Set(Object.keys(Object.assign({}, td.a, td.b, td.want))); | ||
if (opTypes.has("copy")) { | ||
continue; | ||
} | ||
if (opTypes.has("rename")) { | ||
continue; | ||
} | ||
if (opTypes.has("create")) { | ||
continue; | ||
} | ||
if (opTypes.has("delete")) { | ||
continue; | ||
} | ||
if (opTypes.has("truncate")) { | ||
continue; | ||
} | ||
if (opTypes.has("sel")) { | ||
@@ -58,3 +48,3 @@ continue; | ||
}); | ||
describe("transform", () => { | ||
describe("transform:", () => { | ||
const tests = otTestCases_1.transformTests; | ||
@@ -69,4 +59,5 @@ const testTransform = (title, t) => { | ||
const { a1: gotA1, b1: gotB1 } = workspace_1.transform(a, b); | ||
assertOpsEqual("a1", gotA1, a1); | ||
assertOpsEqual("b1", gotB1, b1); | ||
const msg = stringify_1.stringify `transform\n a: ${a}\n b: ${b}\n`; | ||
assertOpsEqual("a1", gotA1, a1, msg); | ||
assertOpsEqual("b1", gotB1, b1, msg); | ||
} | ||
@@ -78,20 +69,2 @@ }); | ||
const opTypes = new Set(Object.keys(Object.assign({}, td.a, td.b, td.a1, td.b1))); | ||
if (opTypes.has("save")) { | ||
continue; | ||
} | ||
if (opTypes.has("copy")) { | ||
continue; | ||
} | ||
if (opTypes.has("rename")) { | ||
continue; | ||
} | ||
if (opTypes.has("create")) { | ||
continue; | ||
} | ||
if (opTypes.has("delete")) { | ||
continue; | ||
} | ||
if (opTypes.has("truncate")) { | ||
continue; | ||
} | ||
if (opTypes.has("sel")) { | ||
@@ -103,5 +76,5 @@ continue; | ||
}); | ||
function assertOpsEqual(label, got, want) { | ||
assert.equal(JSON.stringify(got), JSON.stringify(want), `${label}: got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`); | ||
function assertOpsEqual(label, got, want, msg = "") { | ||
assert.equal(orderedStringify(got), orderedStringify(want), msg + stringify_1.stringify `${label} got: ${got}\n want: ${want}`); | ||
} | ||
//# sourceMappingURL=workspace_test.js.map |
import { EditOps } from "./textDoc"; | ||
export interface WorkspaceOp { | ||
save?: string[]; | ||
copy?: { | ||
@@ -10,2 +9,3 @@ [file: string]: string; | ||
}; | ||
save?: string[]; | ||
create?: string[]; | ||
@@ -39,2 +39,4 @@ delete?: string[]; | ||
export declare function from(a: WorkspaceOp): WorkspaceOp; | ||
export declare function opExists(op: WorkspaceOp, path: string): [boolean, boolean]; | ||
export declare function opExisted(op: WorkspaceOp, path: string): [boolean, boolean]; | ||
export declare function compose(a: WorkspaceOp, b: WorkspaceOp): WorkspaceOp; | ||
@@ -41,0 +43,0 @@ export declare function composeAll(ops: WorkspaceOp[]): WorkspaceOp; |
"use strict"; | ||
const cloneDeep = require("lodash/cloneDeep"); | ||
const includes = require("lodash/includes"); | ||
const invert = require("lodash/invert"); | ||
const invertBy = require("lodash/invertBy"); | ||
const uniq = require("lodash/uniq"); | ||
const values = require("lodash/values"); | ||
const without = require("lodash/without"); | ||
const textDoc_1 = require("./textDoc"); | ||
const stringify_1 = require("../util/stringify"); | ||
function emptyArray(v) { | ||
@@ -25,5 +33,2 @@ return v === undefined || (typeof v.length === "number" && v.length === 0); | ||
validate(op); | ||
if (emptyArray(op.save)) { | ||
delete op.save; | ||
} | ||
if (emptyMap(op.copy)) { | ||
@@ -35,2 +40,5 @@ delete op.copy; | ||
} | ||
if (emptyArray(op.save)) { | ||
delete op.save; | ||
} | ||
if (emptyArray(op.create)) { | ||
@@ -54,2 +62,14 @@ delete op.create; | ||
} | ||
if (op.save) { | ||
op.save = uniq(op.save).sort(); | ||
} | ||
if (op.create) { | ||
op.create = uniq(op.create).sort(); | ||
} | ||
if (op.delete) { | ||
op.delete = uniq(op.delete).sort(); | ||
} | ||
if (op.truncate) { | ||
op.truncate = uniq(op.truncate).sort(); | ||
} | ||
return op; | ||
@@ -102,41 +122,88 @@ } | ||
const op = {}; | ||
if (a.save) { | ||
op.save = a.save.slice(); | ||
} | ||
if (a.copy) { | ||
op.copy = Object.assign({}, a.copy); | ||
op.copy = op.copy || {}; | ||
for (const d of Object.keys(a.copy)) { | ||
const [exists, known] = opExists(op, d); | ||
if (known && exists) { | ||
throw new Error(`copy to: file ${d} file already exists`); | ||
} | ||
op.copy[d] = a.copy[d]; | ||
} | ||
} | ||
if (a.rename) { | ||
op.rename = Object.assign({}, a.rename); | ||
return cloneDeep(a); | ||
} | ||
exports.from = from; | ||
function opExists(op, path) { | ||
let [copiedFrom, copiedTo, renamedFrom, renamedTo, created, deleted, truncated, edited, selected, savedTo] = Array(10).fill(false); | ||
if (op.copy) { | ||
copiedFrom = includes(values(op.copy), path); | ||
copiedTo = includes(Object.keys(op.copy), path); | ||
} | ||
if (a.create) { | ||
op.create = a.create.slice(); | ||
if (op.rename) { | ||
renamedFrom = includes(Object.keys(op.rename), path); | ||
renamedTo = includes(values(op.rename), path); | ||
} | ||
if (a.delete) { | ||
op.delete = a.delete.slice(); | ||
if (op.create) { | ||
created = includes(op.create, path); | ||
} | ||
if (a.truncate) { | ||
op.truncate = a.truncate.slice(); | ||
if (op.delete) { | ||
deleted = includes(op.delete, path); | ||
} | ||
if (a.edit) { | ||
op.edit = Object.assign({}, a.edit); | ||
if (op.truncate) { | ||
truncated = includes(op.truncate, path); | ||
} | ||
if (a.sel) { | ||
op.sel = Object.assign({}, a.sel); | ||
if (op.edit) { | ||
edited = includes(Object.keys(op.edit), path); | ||
} | ||
return op; | ||
if (op.sel) { | ||
selected = includes(Object.keys(op.sel), path); | ||
} | ||
if (op.save) { | ||
if (isFilePath(path)) { | ||
const b = fileToBufferPath(path); | ||
savedTo = includes(op.save, b); | ||
} | ||
} | ||
const exists = copiedFrom || copiedTo || renamedTo || created || truncated || edited || selected || savedTo && !(renamedFrom || deleted); | ||
const known = copiedFrom || copiedTo || renamedFrom || renamedTo || created || deleted || truncated || savedTo || edited || selected; | ||
return [exists, known]; | ||
} | ||
exports.from = from; | ||
exports.opExists = opExists; | ||
function opExisted(op, path) { | ||
let [copiedFrom, copiedTo, renamedFrom, renamedTo, created, deleted, truncated, edited, selected] = Array(9).fill(false); | ||
if (op.copy) { | ||
copiedFrom = includes(values(op.copy), path); | ||
copiedTo = includes(Object.keys(op.copy), path); | ||
} | ||
if (op.rename) { | ||
renamedFrom = includes(Object.keys(op.rename), path); | ||
renamedTo = includes(values(op.rename), path); | ||
} | ||
if (op.create) { | ||
created = includes(op.create, path); | ||
} | ||
if (op.delete) { | ||
deleted = includes(op.delete, path); | ||
} | ||
if (op.truncate) { | ||
truncated = includes(op.truncate, path); | ||
} | ||
if (op.edit) { | ||
edited = includes(Object.keys(op.edit), path); | ||
} | ||
if (op.sel) { | ||
selected = includes(Object.keys(op.sel), path); | ||
} | ||
const existed = copiedFrom || renamedFrom || deleted || truncated || edited || selected && !(renamedTo || copiedTo || created); | ||
const known = copiedFrom || copiedTo || renamedFrom || renamedTo || created || deleted || truncated || edited || selected; | ||
return [existed, known]; | ||
} | ||
exports.opExisted = opExisted; | ||
function compose(a, b) { | ||
validate(a); | ||
validate(b); | ||
if (noop(a)) { | ||
return b; | ||
} | ||
if (noop(b)) { | ||
return a; | ||
} | ||
let op = from(a); | ||
const op = from(a); | ||
const renamedTo = (op.rename ? invert(op.rename) : {}); | ||
const composeCopy = (op, b) => { | ||
const ab = op.copy || {}; | ||
op.copy = op.copy || {}; | ||
if (emptyMap(b.copy)) { | ||
@@ -147,13 +214,117 @@ return; | ||
if (b.copy.hasOwnProperty(d)) { | ||
const s = b.copy[d]; | ||
let s = b.copy[d]; | ||
let [exists, known] = opExists(op, s); | ||
if (known && !exists) { | ||
throw new Error(`copy from: file ${s} does not exist`); | ||
} | ||
[exists, known] = opExists(op, d); | ||
if (known && exists) { | ||
throw new Error(`copy to: file ${d} file already exists`); | ||
} | ||
if (op.edit && op.edit[s]) { | ||
op.edit[d] = op.edit[s]; | ||
} | ||
ab[d] = s; | ||
if (op.copy && op.copy[s]) { | ||
s = op.copy[s]; | ||
} | ||
if (op.rename) { | ||
if (renamedTo[s]) { | ||
s = renamedTo[s]; | ||
} | ||
} | ||
let created = false; | ||
if (op.create && includes(op.create, s)) { | ||
created = true; | ||
op.create.push(d); | ||
} | ||
if (op.delete && includes(op.delete, d)) { | ||
op.delete = without(op.delete, d); | ||
} | ||
if (op.truncate && includes(op.truncate, s)) { | ||
op.truncate.push(d); | ||
} | ||
if (!created) { | ||
op.copy[d] = s; | ||
} | ||
if (isFilePath(s) && isFilePath(d)) { | ||
const sb = fileToBufferPath(s); | ||
const db = fileToBufferPath(d); | ||
if (op.save && op.save.indexOf(sb) !== -1) { | ||
op.save.push(db); | ||
op.copy[db] = sb; | ||
delete op.copy[d]; | ||
} | ||
} | ||
} | ||
} | ||
op.copy = ab; | ||
}; | ||
const composeRename = (op, b) => { | ||
op.rename = op.rename || {}; | ||
if (emptyMap(b.rename)) { | ||
return; | ||
} | ||
for (let s in b.rename) { | ||
if (b.rename.hasOwnProperty(s)) { | ||
const d = b.rename[s]; | ||
let [exists, known] = opExists(op, s); | ||
if (known && !exists) { | ||
throw new Error(`rename from: file ${s} does not exist`); | ||
} | ||
[exists, known] = opExists(op, d); | ||
if (known && exists) { | ||
throw new Error(`rename to: file ${d} file already exists`); | ||
} | ||
const o2 = s; | ||
if (renamedTo[s]) { | ||
const s2 = renamedTo[s]; | ||
s = s2; | ||
} | ||
if (op.copy && op.copy[s]) { | ||
op.copy[d] = op.copy[s]; | ||
delete op.copy[s]; | ||
continue; | ||
} | ||
let created = false; | ||
if (op.create && includes(op.create, s)) { | ||
created = true; | ||
op.create = without(op.create, s); | ||
op.create.push(d); | ||
} | ||
if (op.delete && includes(op.delete, d)) { | ||
op.delete = without(op.delete, d); | ||
} | ||
if (op.truncate && includes(op.truncate, s)) { | ||
op.truncate = without(op.truncate, s); | ||
op.truncate.push(d); | ||
} | ||
if (op.edit && op.edit[o2]) { | ||
op.edit[d] = op.edit[o2]; | ||
delete op.edit[o2]; | ||
delete op.edit[s]; | ||
} | ||
if (op.edit && b.edit && op.edit[b.rename[o2]]) { | ||
op.edit[d] = textDoc_1.composeEdits(op.edit[d], b.edit[b.rename[o2]]); | ||
delete b.edit[b.rename[o2]]; | ||
} | ||
if (!created) { | ||
op.rename[s] = d; | ||
renamedTo[d] = s; | ||
} | ||
if (isFilePath(s)) { | ||
const sb = fileToBufferPath(s); | ||
const db = fileToBufferPath(d); | ||
if (op.save && includes(op.save, sb)) { | ||
op.save.push(db); | ||
op.copy = op.copy || {}; | ||
op.copy[db] = sb; | ||
op.delete = op.delete || []; | ||
op.delete.push(s); | ||
delete op.rename[s]; | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
const composeSave = (op, b) => { | ||
const ab = op.save || []; | ||
op.save = op.save || []; | ||
if (emptySave(b)) { | ||
@@ -164,42 +335,175 @@ return; | ||
const d = bufferToFilePath(s); | ||
let save = true; | ||
const [exists, known] = opExists(op, s); | ||
if (known && !exists) { | ||
throw new Error(`save from: file ${s} does not exist`); | ||
} | ||
if (op.edit && op.edit[d]) { | ||
delete op.edit[d]; | ||
} | ||
if (op.edit && op.edit[s]) { | ||
op.edit = op.edit || {}; | ||
op.edit[d] = op.edit[s]; | ||
} | ||
else if (op.edit && op.edit[d]) { | ||
delete op.edit[d]; | ||
if (op.delete && includes(op.delete, d)) { | ||
op.delete = without(op.delete, d); | ||
} | ||
if (op.create && op.create.indexOf(d) !== -1) { | ||
op.create = op.create.filter(f => f !== d); | ||
if (op.truncate && includes(op.truncate, d)) { | ||
op.truncate = without(op.truncate, d); | ||
} | ||
if (op.delete && op.delete.indexOf(d) !== -1) { | ||
op.delete = op.delete.filter(f => f !== d); | ||
const truncated = Boolean(op.truncate && includes(op.truncate, s)); | ||
if (truncated) { | ||
op.truncate.push(d); | ||
} | ||
if (op.copy && op.copy[s] === d) { | ||
delete op.copy[s]; | ||
if (op.edit) { | ||
delete op.edit[s]; | ||
if (renamedTo[d]) { | ||
const r = renamedTo[d]; | ||
delete op.rename[r]; | ||
op.delete = op.delete || []; | ||
op.delete.push(r); | ||
} | ||
if (op.copy && op.copy[s]) { | ||
const cs = op.copy[s]; | ||
if (isFilePath(cs) && isBufferPath(s) && stripFileOrBufferPathPrefix(cs) === stripFileOrBufferPathPrefix(s)) { | ||
delete op.copy[s]; | ||
op.save = without(op.save, s); | ||
if (op.edit && op.edit[s]) { | ||
op.edit[d] = op.edit[s]; | ||
delete op.edit[s]; | ||
} | ||
continue; | ||
} | ||
save = false; | ||
} | ||
if (save) { | ||
ab.push(s); | ||
if (op.create && includes(op.create, d)) { | ||
op.create = without(op.create, d); | ||
} | ||
if (!truncated) { | ||
op.save.push(s); | ||
} | ||
} | ||
op.save = ab; | ||
}; | ||
const bDelete = new Set(b.delete); | ||
const noDelete = new Set(); | ||
const composeCreate = (op, b) => { | ||
op.create = op.create || []; | ||
if (emptyArray(b)) { | ||
return; | ||
} | ||
for (let f of b) { | ||
const [exists, known] = opExists(op, f); | ||
if (known && exists) { | ||
throw new Error(`create: file ${f} already exist`); | ||
} | ||
if (op.delete && includes(op.delete, f)) { | ||
op.delete = without(op.delete, f); | ||
op.truncate = op.truncate || []; | ||
op.truncate.push(f); | ||
continue; | ||
} | ||
let [chained, renamed] = [false, false]; | ||
let o = ""; | ||
if (op.rename && op.rename[f]) { | ||
renamed = true; | ||
chained = renamed; | ||
o = op.rename[f]; | ||
} | ||
if (!chained) { | ||
if (op.copy && op.copy[f]) { | ||
chained = true; | ||
o = op.copy[f]; | ||
} | ||
} | ||
if (chained) { | ||
if (op.delete && includes(op.delete, o)) { | ||
continue; | ||
} | ||
if (renamed) { | ||
if (bDelete.has(o)) { | ||
op.truncate = op.truncate || []; | ||
op.truncate.push(f); | ||
} | ||
else { | ||
op.create.push(f); | ||
} | ||
noDelete.add(f); | ||
continue; | ||
} | ||
f = o; | ||
} | ||
op.create.push(f); | ||
} | ||
}; | ||
const composeDelete = (op, b) => { | ||
const ab = op.delete || []; | ||
op.delete = op.delete || []; | ||
if (emptyArray(b)) { | ||
return; | ||
} | ||
for (const f of b) { | ||
if (op.edit && op.edit[f]) { | ||
for (let f of b) { | ||
const [exists, known] = opExists(op, f); | ||
if (known && !exists) { | ||
throw new Error(`delete: file ${f} does not exist`); | ||
} | ||
if (op.rename && renamedTo[f]) { | ||
const o = renamedTo[f]; | ||
delete op.rename[o]; | ||
if (op.create && includes(op.create, f)) { | ||
op.create = without(op.create, f); | ||
op.truncate = op.truncate || []; | ||
op.truncate.push(o); | ||
} | ||
else if (!noDelete.has(o)) { | ||
op.delete.push(o); | ||
} | ||
} | ||
else if (op.create && includes(op.create, f)) { | ||
op.create = without(op.create, f); | ||
} | ||
else if (op.copy && op.copy[f]) { | ||
delete op.copy[f]; | ||
} | ||
else { | ||
op.delete.push(f); | ||
} | ||
if (op.rename && op.rename[f]) { | ||
f = op.rename[f]; | ||
} | ||
if (op.edit) { | ||
delete op.edit[f]; | ||
} | ||
ab.push(f); | ||
if (op.sel) { | ||
delete op.sel[f]; | ||
} | ||
if (isFilePath(f)) { | ||
const sb = fileToBufferPath(f); | ||
if (op.save && includes(op.save, sb)) { | ||
op.save = without(op.save, sb); | ||
} | ||
} | ||
} | ||
op.delete = ab; | ||
}; | ||
const composeTruncate = (op, b) => { | ||
op.truncate = op.truncate || []; | ||
if (emptyArray(b)) { | ||
return; | ||
} | ||
for (let f of b) { | ||
const [exists, known] = opExists(op, f); | ||
if (known && !exists) { | ||
throw new Error(`truncate: file ${f} does not exist`); | ||
} | ||
if (op.rename && op.rename[f]) { | ||
f = op.rename[f]; | ||
} | ||
op.truncate.push(f); | ||
if (op.edit) { | ||
delete op.edit[f]; | ||
} | ||
if (op.sel) { | ||
delete op.sel[f]; | ||
} | ||
if (isFilePath(f)) { | ||
const sb = fileToBufferPath(f); | ||
if (op.save && includes(op.save, sb)) { | ||
op.save = without(op.save, sb); | ||
} | ||
} | ||
} | ||
}; | ||
const composeEdit = (op, b) => { | ||
@@ -209,9 +513,16 @@ if (emptyEdit(b)) { | ||
} | ||
const ab = op.edit || {}; | ||
for (const fb in b) { | ||
if (b.hasOwnProperty(fb)) { | ||
ab[fb] = textDoc_1.composeEdits(ab[fb] || [], b[fb]); | ||
op.edit = op.edit || {}; | ||
for (const f of Object.keys(b)) { | ||
const [exists, known] = opExists(op, f); | ||
if (known && !exists) { | ||
throw new Error(`edit: file ${f} does not exist`); | ||
} | ||
op.edit[f] = textDoc_1.composeEdits(op.edit[f] || [], b[f]); | ||
if (op.create && includes(op.create, f)) { | ||
const { ret: ret } = textDoc_1.countEdits(op.edit[f]); | ||
if (ret !== 0) { | ||
throw new Error(`newly created file has nonzero retain count ${f}`); | ||
} | ||
} | ||
} | ||
op.edit = ab; | ||
}; | ||
@@ -231,10 +542,53 @@ const composeSel = (op, b) => { | ||
composeCopy(op, b); | ||
composeRename(op, b); | ||
composeSave(op, b.save); | ||
composeCreate(op, b.create); | ||
composeDelete(op, b.delete); | ||
composeTruncate(op, b.truncate); | ||
composeEdit(op, b.edit); | ||
composeSel(op, b.sel); | ||
op.head = b.head || a.head; | ||
if (op.copy) { | ||
for (const d of Object.keys(op.copy)) { | ||
const s = op.copy[d]; | ||
if (d === s) { | ||
delete op.copy[d]; | ||
} | ||
} | ||
} | ||
if (op.rename) { | ||
for (const s of Object.keys(op.rename)) { | ||
const d = op.rename[s]; | ||
if (s === d) { | ||
delete op.rename[s]; | ||
} | ||
} | ||
} | ||
if (op.truncate) { | ||
const toRemove = []; | ||
for (const f of op.truncate) { | ||
if (op.create && includes(op.create, f)) { | ||
toRemove.push(f); | ||
} | ||
} | ||
for (const r of toRemove) { | ||
op.truncate = without(op.truncate, r); | ||
} | ||
} | ||
if (op.save) { | ||
const toRemove = []; | ||
for (const s of op.save) { | ||
const d = bufferToFilePath(s); | ||
if (op.delete && includes(op.delete, d)) { | ||
toRemove.push(s); | ||
} | ||
} | ||
for (const r of toRemove) { | ||
op.save = without(op.save, r); | ||
} | ||
} | ||
return normalize(op); | ||
} | ||
exports.compose = compose; | ||
; | ||
function composeAll(ops) { | ||
@@ -286,2 +640,170 @@ if (!ops) { | ||
}; | ||
const yCopyFrom = invertBy(y.copy || {}); | ||
const newFileSrc = {}; | ||
const transformCopy = (x, y, z) => { | ||
if (!x.copy) { | ||
x.copy = {}; | ||
} | ||
if (!y.copy) { | ||
y.copy = {}; | ||
} | ||
for (const d of Object.keys(x.copy)) { | ||
const s = x.copy[d]; | ||
if (y.copy[d] && y.copy[d] === s) { | ||
continue; | ||
} | ||
else if (y.copy[d] && y.copy[d] !== s) { | ||
throw new Error(`copy to ${s}: confilict: ${y.copy[d]}`); | ||
} | ||
const [srcExisted, srcKnown] = opExisted(y, s); | ||
if (srcKnown && !srcExisted) { | ||
throw new Error(`copy: ${s} does not exist`); | ||
} | ||
const [destExists, destKnown] = opExists(y, d); | ||
if (y.rename && y.rename[s] === d) { | ||
continue; | ||
} | ||
else if (destExists && destKnown) { | ||
throw new Error(`copy: file ${d} already exists`); | ||
} | ||
z.copy = z.copy || {}; | ||
z.copy[d] = s; | ||
newFileSrc[d] = s; | ||
} | ||
}; | ||
const transformRename = (x, y, z) => { | ||
if (!x.rename) { | ||
x.rename = {}; | ||
} | ||
if (!y.rename) { | ||
y.rename = {}; | ||
} | ||
for (const s of Object.keys(x.rename)) { | ||
const d = x.rename[s]; | ||
if (y.rename[d] && y.rename[d] === s) { | ||
continue; | ||
} | ||
else if (y.rename[d] && y.rename[d] !== s) { | ||
throw new Error(`rename to: ${s} conflict: ${y.rename[d]}`); | ||
} | ||
const [srcExisted, srcKnown] = opExisted(y, s); | ||
if (!srcExisted && srcKnown) { | ||
throw new Error(`rename from: ${d} does not exist`); | ||
} | ||
if (y.rename[s] && y.rename[s] === d) { | ||
continue; | ||
} | ||
else if (y.rename[s]) { | ||
z.copy = z.copy || {}; | ||
z.copy[d] = y.rename[s]; | ||
newFileSrc[y.rename[s]] = s; | ||
continue; | ||
} | ||
const [srcExists] = opExists(y, s); | ||
if (srcKnown && !srcExists) { | ||
throw new Error(`rename from: ${s} does not exist`); | ||
} | ||
const [destExisted, destKnown] = opExisted(y, d); | ||
const [destExists, destKnown2] = opExists(y, d); | ||
if (y.copy && y.copy[d] && y.copy[d] === s) { | ||
z.delete = z.delete || []; | ||
z.delete.push(s); | ||
continue; | ||
} | ||
else if (destExisted && destKnown) { | ||
throw new Error(`rename to: ${d} already exists`); | ||
} | ||
else if (destExists && destKnown2) { | ||
throw new Error(`rename to: ${d} already exists`); | ||
} | ||
z.rename = z.rename || {}; | ||
z.rename[s] = d; | ||
newFileSrc[d] = s; | ||
} | ||
}; | ||
const transformSave = (x, y, z) => { | ||
if (!x.save) { | ||
x.save = []; | ||
} | ||
if (!y.save) { | ||
y.save = []; | ||
} | ||
z.save = z.save || []; | ||
for (const s of x.save) { | ||
if (y.save && includes(y.save, s)) { | ||
continue; | ||
} | ||
z.save.push(s); | ||
} | ||
}; | ||
const transformCreate = (x, y, z) => { | ||
if (!x.create) { | ||
x.create = []; | ||
} | ||
if (!y.create) { | ||
y.create = []; | ||
} | ||
z.create = z.create || []; | ||
for (const f of x.create) { | ||
if (y.create && !includes(y.create, f)) { | ||
const [exists, known] = opExists(y, f); | ||
if (known && exists) { | ||
throw new Error(`create: file ${f} already exists`); | ||
} | ||
z.create.push(f); | ||
} | ||
if (y.delete && includes(y.delete, f)) { | ||
throw new Error(`create: conflict: y delete (file ${f})`); | ||
} | ||
} | ||
}; | ||
const transformDelete = (x, y, z) => { | ||
if (!x.delete) { | ||
x.delete = []; | ||
} | ||
if (!y.delete) { | ||
y.delete = []; | ||
} | ||
z.delete = z.delete || []; | ||
for (const f of x.delete) { | ||
if (y.delete && !includes(y.delete, f)) { | ||
const [exists, known] = opExists(y, f); | ||
if (known && !exists) { | ||
throw new Error(`delete: file ${f} does not exist`); | ||
} | ||
z.delete.push(f); | ||
} | ||
if (y.create && includes(y.create, f)) { | ||
throw new Error(`delete: conflict: y create (file ${f})`); | ||
} | ||
} | ||
}; | ||
const transformTruncate = (x, y, z) => { | ||
if (!x.truncate) { | ||
x.truncate = []; | ||
} | ||
if (!y.truncate) { | ||
y.truncate = []; | ||
} | ||
z.truncate = z.truncate || []; | ||
for (const f of x.truncate) { | ||
const ycreated = Boolean(y.create && includes(y.create, f)); | ||
const ydeleted = Boolean(y.delete && includes(y.delete, f)); | ||
const ytruncated = includes(y.truncate, f); | ||
const sameTruncateEdit = Boolean(ytruncated && y.edit && x.edit && stringify_1.orderedStringify(y.edit[f]) === stringify_1.orderedStringify(x.edit[f])); | ||
if (y.edit && Object.keys(y.edit[f]).length > 0 && !sameTruncateEdit) { | ||
throw new Error(`truncate: conflict: edit and y truncate (file ${f})`); | ||
} | ||
if (ydeleted) { | ||
throw new Error(`truncate: conflict: y delete (file ${f})`); | ||
} | ||
if (!ytruncated && !ycreated && !ydeleted && !sameTruncateEdit) { | ||
const [exists, known] = opExists(y, f); | ||
if (known && !exists) { | ||
throw new Error(`truncate: ${f} does not exist`); | ||
} | ||
z.truncate.push(f); | ||
} | ||
} | ||
}; | ||
const transformEdit = (x, y, z) => { | ||
@@ -294,22 +816,71 @@ if (!x.edit) { | ||
} | ||
const ab = copyHACK(x.edit); | ||
for (const f in x.edit) { | ||
z.edit = z.edit || {}; | ||
for (let f in x.edit) { | ||
if (x.edit.hasOwnProperty(f)) { | ||
const xedit = x.edit[f]; | ||
const yedit = y.edit[f]; | ||
let edit; | ||
if (yedit) { | ||
edit = transformEditOps(copyHACK(x.edit[f]), copyHACK(yedit), primary); | ||
let edit = x.edit[f]; | ||
if (y.rename && y.rename[f]) { | ||
const [exists, known] = opExists(y, y.rename[f]); | ||
if (known && !exists) { | ||
throw new Error(`edit: file ${f} does not exist`); | ||
} | ||
} | ||
else { | ||
const [exists, known] = opExists(y, f); | ||
if (known && !exists) { | ||
throw new Error(`edit: file ${f} does not exist`); | ||
} | ||
} | ||
if (yCopyFrom[f]) { | ||
for (const d of yCopyFrom[f]) { | ||
z.edit[d] = transformEditOps(copyHACK(x.edit[f] || []), copyHACK(y.edit[d] || []), primary); | ||
} | ||
} | ||
let yf; | ||
if (newFileSrc[f]) { | ||
yf = newFileSrc[f]; | ||
} | ||
else { | ||
yf = f; | ||
} | ||
if (y.rename && y.rename[yf]) { | ||
const d = y.rename[yf]; | ||
f = d; | ||
edit = transformEditOps(copyHACK(x.edit[f] || []), copyHACK(y.edit[d] || []), primary); | ||
} | ||
if (y.edit[yf]) { | ||
edit = transformEditOps(copyHACK(edit || []), copyHACK(y.edit[yf]), primary); | ||
} | ||
if (JSON.stringify(xedit) !== JSON.stringify(yedit)) { | ||
if (z.edit[f] && Object.keys(z.edit[f]).length !== 0) { | ||
throw new Error(`copy/rename edit conflict`); | ||
} | ||
if (edit) { | ||
ab[f] = edit; | ||
z.edit[f] = edit; | ||
} | ||
} | ||
else { | ||
delete ab[f]; | ||
} | ||
} | ||
for (let f in x.edit) { | ||
if (isBufferPath(f)) { | ||
const d = bufferToFilePath(f); | ||
if (y.save && includes(y.save, f)) { | ||
z.edit[d] = transformEditOps(copyHACK(x.edit[f] || []), copyHACK(y.edit[d] || []), primary); | ||
} | ||
if (x.save && includes(x.save, f)) { | ||
z.edit[d] = y.edit[d]; | ||
} | ||
} | ||
} | ||
z.edit = ab; | ||
for (let f in x.edit) { | ||
if (isFilePath(f)) { | ||
const s = fileToBufferPath(f); | ||
if (x.save && includes(x.save, s)) { | ||
z.edit[f] = transformEditOps(copyHACK(x.edit[f] || []), copyHACK(y.edit[s] || []), primary); | ||
const z1 = transformEditOps(copyHACK(z.edit[f] || []), copyHACK(y.edit[f] || []), primary); | ||
z.edit[f] = textDoc_1.composeEdits(copyHACK(y.edit[f] || []), z1); | ||
} | ||
} | ||
} | ||
}; | ||
@@ -337,2 +908,8 @@ const transformSel = (x, y, z) => { | ||
const z = {}; | ||
transformCopy(x, y, z); | ||
transformRename(x, y, z); | ||
transformSave(x, y, z); | ||
transformCreate(x, y, z); | ||
transformDelete(x, y, z); | ||
transformTruncate(x, y, z); | ||
transformEdit(x, y, z); | ||
@@ -339,0 +916,0 @@ transformSel(x, y, z); |
@@ -7,3 +7,3 @@ "use strict"; | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments)).next()); | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
@@ -10,0 +10,0 @@ }; |
@@ -7,3 +7,3 @@ "use strict"; | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments)).next()); | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
@@ -10,0 +10,0 @@ }; |
{ | ||
"name": "libzap", | ||
"version": "0.0.38", | ||
"version": "0.0.39", | ||
"description": "JavaScript library for Zap", | ||
@@ -23,2 +23,3 @@ "license": "none", | ||
"diff-match-patch": "^1.0.0", | ||
"lodash": "^4.17.2", | ||
"uuid": "^3.0.1", | ||
@@ -29,2 +30,3 @@ "vscode-jsonrpc": "3.0.1-alpha.7" | ||
"@types/diff-match-patch": "^1.0.31", | ||
"@types/lodash": "4.14.39", | ||
"@types/mocha": "^2.2.32", | ||
@@ -31,0 +33,0 @@ "@types/node": "^6.0.42", |
@@ -461,2 +461,5 @@ // Code generated by ot/gen/gen-ts.go; DO NOT EDIT! | ||
"want": { | ||
"copy": { | ||
"#f2": "#f1" | ||
}, | ||
"save": [ | ||
@@ -466,5 +469,2 @@ "#f1", | ||
], | ||
"copy": { | ||
"#f2": "#f1" | ||
}, | ||
"edit": { | ||
@@ -489,8 +489,8 @@ "#f1": [ | ||
"want": { | ||
"copy": { | ||
"#f": "/f" | ||
}, | ||
"save": [ | ||
"#f" | ||
], | ||
"copy": { | ||
"#f": "/f" | ||
} | ||
] | ||
} | ||
@@ -510,9 +510,9 @@ }, | ||
"want": { | ||
"copy": { | ||
"#f2": "#f1" | ||
}, | ||
"save": [ | ||
"#f1", | ||
"#f2" | ||
], | ||
"copy": { | ||
"#f2": "#f1" | ||
} | ||
] | ||
} | ||
@@ -1507,8 +1507,8 @@ }, | ||
"a": { | ||
"rename": { | ||
"/f1": "/f2" | ||
}, | ||
"save": [ | ||
"#f1" | ||
], | ||
"rename": { | ||
"/f1": "/f2" | ||
} | ||
] | ||
}, | ||
@@ -2128,8 +2128,8 @@ "b": { | ||
"want": { | ||
"copy": { | ||
"#f2": "#f1" | ||
}, | ||
"save": [ | ||
"#f2" | ||
], | ||
"copy": { | ||
"#f2": "#f1" | ||
}, | ||
"delete": [ | ||
@@ -2450,8 +2450,8 @@ "/f1" | ||
"want": { | ||
"copy": { | ||
"/f2": "/f1" | ||
}, | ||
"save": [ | ||
"#f1" | ||
], | ||
"copy": { | ||
"/f2": "/f1" | ||
} | ||
] | ||
} | ||
@@ -2657,8 +2657,8 @@ }, | ||
"want": { | ||
"rename": { | ||
"/f1": "/f2" | ||
}, | ||
"save": [ | ||
"#f1" | ||
], | ||
"rename": { | ||
"/f1": "/f2" | ||
} | ||
] | ||
} | ||
@@ -2793,16 +2793,16 @@ }, | ||
"b": { | ||
"rename": { | ||
"/f1": "/f2" | ||
}, | ||
"save": [ | ||
"#f1" | ||
], | ||
] | ||
}, | ||
"want": { | ||
"rename": { | ||
"/f1": "/f2" | ||
} | ||
}, | ||
"want": { | ||
}, | ||
"save": [ | ||
"#f1" | ||
], | ||
"rename": { | ||
"/f1": "/f2" | ||
}, | ||
"edit": { | ||
@@ -3172,3 +3172,3 @@ "#f1": [ | ||
"truncate": [ | ||
"a" | ||
"/a" | ||
] | ||
@@ -3178,3 +3178,3 @@ }, | ||
"truncate": [ | ||
"b" | ||
"/b" | ||
] | ||
@@ -3184,4 +3184,4 @@ }, | ||
"truncate": [ | ||
"a", | ||
"b" | ||
"/a", | ||
"/b" | ||
] | ||
@@ -3188,0 +3188,0 @@ }, |
import * as assert from "assert"; | ||
import { WorkspaceOp, compose, noop, transform } from "./workspace"; | ||
import { stringify } from "../util/stringify"; | ||
// Most of the test cases used here are generated from the corresponding tests | ||
// in the ot go package. If you want to modify/add a test, do it in | ||
// ot/ot_test_cases.go and run the generate script. | ||
import { composeTests, transformTests } from "./otTestCases"; | ||
function orderedStringify(obj: Object): string { | ||
return JSON.stringify(obj, Object.keys(obj).sort()); | ||
} | ||
describe("noop", () => { | ||
@@ -16,7 +24,7 @@ ([ | ||
] as { op: WorkspaceOp, want: boolean }[]).forEach(({op, want}) => { | ||
it(`${JSON.stringify(op)}`, () => assert.equal(noop(op), want)); | ||
it(stringify`${op}`, () => assert.equal(noop(op), want)); | ||
}); | ||
}); | ||
describe("compose", () => { | ||
describe("compose:", () => { | ||
type testCase = { a: WorkspaceOp, b: WorkspaceOp, want: WorkspaceOp, wantErr: boolean | undefined, commutative: boolean | undefined }; | ||
@@ -29,5 +37,6 @@ const tests = composeTests as { [key: string]: testCase }; | ||
if (wantErr) { | ||
assert.throws(() => compose(a, b)); | ||
assert.throws(() => compose(a, b), stringify`compose:\n a: ${a}\n b: ${b}`); | ||
} else { | ||
assert.deepEqual(compose(a, b), want); | ||
let got = compose(a, b); | ||
assert.deepEqual(got, want, stringify`compose:\n a: ${a}\n b: ${b}\n got: ${got}\n want: ${want}`); | ||
} | ||
@@ -42,7 +51,2 @@ }); | ||
const opTypes = new Set(Object.keys(Object.assign({}, td.a, td.b, td.want))); | ||
if (opTypes.has("copy")) { continue; } | ||
if (opTypes.has("rename")) { continue; } | ||
if (opTypes.has("create")) { continue; } | ||
if (opTypes.has("delete")) { continue; } | ||
if (opTypes.has("truncate")) { continue; } | ||
if (opTypes.has("sel")) { continue; } // TODO(renfred): Some sel cases are broken | ||
@@ -54,3 +58,3 @@ | ||
describe("transform", () => { | ||
describe("transform:", () => { | ||
type testCase = { a: WorkspaceOp, b: WorkspaceOp, a1: WorkspaceOp, b1: WorkspaceOp, wantErr: boolean | undefined }; | ||
@@ -66,4 +70,5 @@ const tests = transformTests as { [key: string]: testCase }; | ||
const {a1: gotA1, b1: gotB1} = transform(a, b); | ||
assertOpsEqual("a1", gotA1, a1); | ||
assertOpsEqual("b1", gotB1, b1); | ||
const msg = stringify`transform\n a: ${a}\n b: ${b}\n`; | ||
assertOpsEqual("a1", gotA1, a1, msg); | ||
assertOpsEqual("b1", gotB1, b1, msg); | ||
} | ||
@@ -78,8 +83,2 @@ }); | ||
const opTypes = new Set(Object.keys(Object.assign({}, td.a, td.b, td.a1, td.b1))); | ||
if (opTypes.has("save")) { continue; } | ||
if (opTypes.has("copy")) { continue; } | ||
if (opTypes.has("rename")) { continue; } | ||
if (opTypes.has("create")) { continue; } | ||
if (opTypes.has("delete")) { continue; } | ||
if (opTypes.has("truncate")) { continue; } | ||
if (opTypes.has("sel")) { continue; } // TODO(renfred): Some sel cases are broken | ||
@@ -91,4 +90,4 @@ | ||
function assertOpsEqual(label: string, got: WorkspaceOp, want: WorkspaceOp) { | ||
assert.equal(JSON.stringify(got), JSON.stringify(want), `${label}: got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`); | ||
function assertOpsEqual(label: string, got: WorkspaceOp, want: WorkspaceOp, msg: string = "") { | ||
assert.equal(orderedStringify(got), orderedStringify(want), msg + stringify`${label} got: ${got}\n want: ${want}`); | ||
} |
@@ -1,7 +0,16 @@ | ||
import { EditOps, composeEdits, transformEdits } from "./textDoc"; | ||
import * as cloneDeep from "lodash/cloneDeep"; | ||
import * as includes from "lodash/includes"; | ||
import * as invert from "lodash/invert"; | ||
import * as invertBy from "lodash/invertBy"; | ||
import * as uniq from "lodash/uniq"; | ||
import * as values from "lodash/values"; | ||
import * as without from "lodash/without"; | ||
import { EditOps, composeEdits, transformEdits, countEdits } from "./textDoc"; | ||
import { orderedStringify } from "../util/stringify"; | ||
export interface WorkspaceOp { | ||
save?: string[]; | ||
copy?: { [file: string]: string }; | ||
rename?: { [file: string]: string }; | ||
save?: string[]; | ||
create?: string[]; | ||
@@ -49,5 +58,5 @@ delete?: string[]; | ||
validate(op); | ||
if (emptyArray(op.save)) { delete op.save; } | ||
if (emptyMap(op.copy)) { delete op.copy; } | ||
if (emptyMap(op.rename)) { delete op.rename; } | ||
if (emptyArray(op.save)) { delete op.save; } | ||
if (emptyArray(op.create)) { delete op.create; } | ||
@@ -59,2 +68,8 @@ if (emptyArray(op.delete)) { delete op.delete; } | ||
if (op.head === undefined) { delete op.head; } | ||
if (op.save) { op.save = uniq(op.save).sort(); } | ||
if (op.create) { op.create = uniq(op.create).sort(); } | ||
if (op.delete) { op.delete = uniq(op.delete).sort(); } | ||
if (op.truncate) { op.truncate = uniq(op.truncate).sort(); } | ||
return op; | ||
@@ -110,29 +125,83 @@ } | ||
const op: WorkspaceOp = {}; | ||
if (a.save) { | ||
op.save = a.save.slice(); | ||
} | ||
if (a.copy) { | ||
op.copy = Object.assign({}, a.copy); | ||
op.copy = op.copy || {}; | ||
for (const d of Object.keys(a.copy)) { | ||
const [exists, known] = opExists(op, d); | ||
if (known && exists) { | ||
throw new Error(`copy to: file ${d} file already exists`); | ||
} | ||
op.copy[d] = a.copy[d]; | ||
} | ||
} | ||
if (a.rename) { | ||
op.rename = Object.assign({}, a.rename); | ||
return cloneDeep(a); | ||
} | ||
export function opExists(op: WorkspaceOp, path: string): [boolean, boolean] { | ||
let [copiedFrom, copiedTo, renamedFrom, renamedTo, created, deleted, truncated, edited, selected, savedTo] = Array(10).fill(false); | ||
if (op.copy) { | ||
copiedFrom = includes(values(op.copy), path); | ||
copiedTo = includes(Object.keys(op.copy), path); | ||
} | ||
if (a.create) { | ||
op.create = a.create.slice(); | ||
if (op.rename) { | ||
renamedFrom = includes(Object.keys(op.rename), path); | ||
renamedTo = includes(values(op.rename), path); | ||
} | ||
if (a.delete) { | ||
op.delete = a.delete.slice(); | ||
if (op.create) { | ||
created = includes(op.create, path); | ||
} | ||
if (a.truncate) { | ||
op.truncate = a.truncate.slice(); | ||
if (op.delete) { | ||
deleted = includes(op.delete, path); | ||
} | ||
if (a.edit) { | ||
op.edit = Object.assign({}, a.edit); | ||
if (op.truncate) { | ||
truncated = includes(op.truncate, path); | ||
} | ||
if (a.sel) { | ||
op.sel = Object.assign({}, a.sel); | ||
if (op.edit) { | ||
edited = includes(Object.keys(op.edit), path); | ||
} | ||
return op; | ||
if (op.sel) { | ||
selected = includes(Object.keys(op.sel), path); | ||
} | ||
if (op.save) { | ||
if (isFilePath(path)) { | ||
const b = fileToBufferPath(path); | ||
savedTo = includes(op.save, b); | ||
} | ||
} | ||
const exists = copiedFrom || copiedTo || renamedTo || created || truncated || edited || selected || savedTo && !(renamedFrom || deleted); | ||
const known = copiedFrom || copiedTo || renamedFrom || renamedTo || created || deleted || truncated || savedTo || edited || selected; | ||
return [exists, known]; | ||
} | ||
export function opExisted(op: WorkspaceOp, path: string): [boolean, boolean] { | ||
let [copiedFrom, copiedTo, renamedFrom, renamedTo, created, deleted, truncated, edited, selected] = Array(9).fill(false); | ||
if (op.copy) { | ||
copiedFrom = includes(values(op.copy), path); | ||
copiedTo = includes(Object.keys(op.copy), path); | ||
} | ||
if (op.rename) { | ||
renamedFrom = includes(Object.keys(op.rename), path); | ||
renamedTo = includes(values(op.rename), path); | ||
} | ||
if (op.create) { | ||
created = includes(op.create, path); | ||
} | ||
if (op.delete) { | ||
deleted = includes(op.delete, path); | ||
} | ||
if (op.truncate) { | ||
truncated = includes(op.truncate, path); | ||
} | ||
if (op.edit) { | ||
edited = includes(Object.keys(op.edit), path); | ||
} | ||
if (op.sel) { | ||
selected = includes(Object.keys(op.sel), path); | ||
} | ||
const existed = copiedFrom || renamedFrom || deleted || truncated || edited || selected && !(renamedTo || copiedTo || created); | ||
const known = copiedFrom || copiedTo || renamedFrom || renamedTo || created || deleted || truncated || edited || selected; | ||
return [existed, known]; | ||
} | ||
// TODO(sqs): how do we need to handle Sel? do we need to adjust it based on edits? | ||
@@ -143,69 +212,326 @@ | ||
validate(b); | ||
if (noop(a)) { return b; } | ||
if (noop(b)) { return a; } | ||
let op: WorkspaceOp = from(a); | ||
const op: WorkspaceOp = from(a); | ||
const renamedTo = (op.rename ? invert(op.rename) : {}) as { [key: string]: string }; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const composeCopy = (op: WorkspaceOp, b: WorkspaceOp): void => { | ||
const ab = op.copy || {}; | ||
op.copy = op.copy || {}; | ||
if (emptyMap(b.copy)) { return; } | ||
for (const d in b.copy) { | ||
if (b.copy.hasOwnProperty(d)) { | ||
const s = b.copy[d]; | ||
let s = b.copy[d]; | ||
let [exists, known] = opExists(op, s); | ||
if (known && !exists) { | ||
throw new Error(`copy from: file ${s} does not exist`); | ||
} | ||
[exists, known] = opExists(op, d); | ||
if (known && exists) { | ||
throw new Error(`copy to: file ${d} file already exists`); | ||
} | ||
if (op.edit && op.edit[s]) { | ||
op.edit[d] = op.edit[s]; | ||
} | ||
ab[d] = s; | ||
if (op.copy && op.copy[s]) { | ||
s = op.copy[s]; | ||
} | ||
if (op.rename) { | ||
if (renamedTo[s]) { | ||
s = renamedTo[s]; | ||
} | ||
} | ||
let created = false; | ||
if (op.create && includes(op.create, s)) { | ||
created = true; | ||
op.create.push(d); | ||
} | ||
if (op.delete && includes(op.delete, d)) { | ||
op.delete = without(op.delete, d); | ||
} | ||
if (op.truncate && includes(op.truncate, s)) { | ||
op.truncate.push(d); | ||
} | ||
if (!created) { | ||
op.copy[d] = s; | ||
// TODO(renfred) update copyFrom here | ||
} | ||
if (isFilePath(s) && isFilePath(d)) { | ||
const sb = fileToBufferPath(s); | ||
const db = fileToBufferPath(d); | ||
if (op.save && op.save.indexOf(sb) !== -1) { | ||
op.save.push(db); | ||
op.copy[db] = sb; | ||
delete op.copy[d]; | ||
} | ||
} | ||
} | ||
} | ||
op.copy = ab; | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const composeRename = (op: WorkspaceOp, b: WorkspaceOp): void => { | ||
op.rename = op.rename || {}; | ||
if (emptyMap(b.rename)) { return; } | ||
for (let s in b.rename) { | ||
if (b.rename.hasOwnProperty(s)) { | ||
const d = b.rename[s]; | ||
let [exists, known] = opExists(op, s); | ||
if (known && !exists) { | ||
throw new Error(`rename from: file ${s} does not exist`); | ||
} | ||
[exists, known] = opExists(op, d); | ||
if (known && exists) { | ||
throw new Error(`rename to: file ${d} file already exists`); | ||
} | ||
const o2 = s; | ||
if (renamedTo[s]) { | ||
const s2 = renamedTo[s]; | ||
s = s2; | ||
} | ||
if (op.copy && op.copy[s]) { | ||
op.copy[d] = op.copy[s]; | ||
delete op.copy[s]; | ||
continue; | ||
} | ||
let created = false; | ||
if (op.create && includes(op.create, s)) { | ||
created = true; | ||
op.create = without(op.create, s); | ||
op.create.push(d); | ||
} | ||
if (op.delete && includes(op.delete, d)) { | ||
op.delete = without(op.delete, d); | ||
} | ||
if (op.truncate && includes(op.truncate, s)) { | ||
op.truncate = without(op.truncate, s); | ||
op.truncate.push(d); | ||
} | ||
if (op.edit && op.edit[o2]) { | ||
op.edit[d] = op.edit[o2]; | ||
delete op.edit[o2]; | ||
delete op.edit[s]; | ||
} | ||
if (op.edit && b.edit && op.edit[b.rename[o2]]) { | ||
op.edit[d] = composeEdits(op.edit[d], b.edit[b.rename[o2]]); | ||
delete b.edit[b.rename[o2]]; | ||
} | ||
// TODO selections | ||
if (!created) { | ||
op.rename[s] = d; | ||
renamedTo[d] = s; | ||
} | ||
if (isFilePath(s)) { | ||
const sb = fileToBufferPath(s); | ||
const db = fileToBufferPath(d); | ||
if (op.save && includes(op.save, sb)) { | ||
op.save.push(db); | ||
op.copy = op.copy || {}; | ||
op.copy[db] = sb; | ||
op.delete = op.delete || []; | ||
op.delete.push(s); | ||
delete op.rename[s]; | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const composeSave = (op: WorkspaceOp, b: FileSaves | undefined): void => { | ||
const ab = op.save || []; | ||
op.save = op.save || []; | ||
if (emptySave(b)) { return; } | ||
for (const s of b) { | ||
const d = bufferToFilePath(s); | ||
let save = true; | ||
if (op.edit && op.edit[s]) { | ||
op.edit = op.edit || {}; | ||
op.edit[d] = op.edit![s]; | ||
} else if (op.edit && op.edit[d]) { | ||
const [exists, known] = opExists(op, s); | ||
if (known && !exists) { | ||
throw new Error(`save from: file ${s} does not exist`); | ||
} | ||
if (op.edit && op.edit[d]) { | ||
delete op.edit[d]; | ||
} | ||
if (op.create && op.create.indexOf(d) !== -1) { op.create = op.create.filter(f => f !== d); } | ||
if (op.delete && op.delete.indexOf(d) !== -1) { op.delete = op.delete.filter(f => f !== d); } | ||
if (op.copy && op.copy[s] === d) { | ||
delete op.copy[s]; | ||
if (op.edit) { delete op.edit[s]; } | ||
save = false; | ||
if (op.edit && op.edit[s]) { | ||
op.edit[d] = op.edit[s]; | ||
} | ||
if (save) { ab.push(s); } | ||
if (op.delete && includes(op.delete, d)) { | ||
op.delete = without(op.delete, d); | ||
} | ||
if (op.truncate && includes(op.truncate, d)) { | ||
op.truncate = without(op.truncate, d); | ||
} | ||
const truncated = Boolean(op.truncate && includes(op.truncate, s)); | ||
if (truncated) { | ||
op.truncate!.push(d); | ||
} | ||
if (renamedTo[d]) { | ||
const r = renamedTo[d]; | ||
delete op.rename![r]; | ||
op.delete = op.delete || []; | ||
op.delete.push(r); | ||
} | ||
if (op.copy && op.copy[s]) { | ||
const cs = op.copy[s]; | ||
if (isFilePath(cs) && isBufferPath(s) && stripFileOrBufferPathPrefix(cs) === stripFileOrBufferPathPrefix(s)) { | ||
delete op.copy[s]; | ||
op.save = without(op.save, s); | ||
if (op.edit && op.edit[s]) { | ||
op.edit[d] = op.edit[s]; | ||
delete op.edit[s]; | ||
} | ||
continue; | ||
} | ||
} | ||
if (op.create && includes(op.create, d)) { | ||
op.create = without(op.create, d); | ||
} | ||
if (!truncated) { | ||
op.save.push(s); | ||
} | ||
} | ||
op.save = ab; | ||
}; | ||
const bDelete = new Set<string>(b.delete); | ||
const noDelete = new Set<string>(); | ||
// tslint:disable-next-line no-shadowed-variable | ||
const composeCreate = (op: WorkspaceOp, b: string[] | undefined): void => { | ||
op.create = op.create || []; | ||
if (emptyArray(b)) { return; } | ||
for (let f of b) { | ||
const [exists, known] = opExists(op, f); | ||
if (known && exists) { | ||
throw new Error(`create: file ${f} already exist`); | ||
} | ||
if (op.delete && includes(op.delete, f)) { | ||
op.delete = without(op.delete, f); | ||
op.truncate = op.truncate || []; | ||
op.truncate.push(f); | ||
continue; | ||
} | ||
let [chained, renamed] = [false, false]; | ||
let o = ""; | ||
if (op.rename && op.rename[f]) { | ||
renamed = true; | ||
chained = renamed; | ||
o = op.rename[f]; | ||
} | ||
if (!chained) { | ||
if (op.copy && op.copy[f]) { | ||
chained = true; | ||
o = op.copy[f]; | ||
} | ||
} | ||
if (chained) { | ||
if (op.delete && includes(op.delete, o)) { | ||
continue; | ||
} | ||
if (renamed) { | ||
if (bDelete.has(o)) { | ||
op.truncate = op.truncate || []; | ||
op.truncate.push(f); | ||
} else { | ||
op.create.push(f); | ||
} | ||
noDelete.add(f); | ||
continue; | ||
} | ||
f = o; | ||
} | ||
op.create.push(f); | ||
} | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const composeDelete = (op: WorkspaceOp, b: string[] | undefined): void => { | ||
const ab = op.delete || []; | ||
op.delete = op.delete || []; | ||
if (emptyArray(b)) { return; } | ||
for (const f of b) { | ||
if (op.edit && op.edit[f]) { delete op.edit[f]; } | ||
ab.push(f); | ||
for (let f of b) { | ||
const [exists, known] = opExists(op, f); | ||
if (known && !exists) { | ||
throw new Error(`delete: file ${f} does not exist`); | ||
} | ||
if (op.rename && renamedTo[f]) { | ||
const o = renamedTo[f]; | ||
delete op.rename[o]; | ||
if (op.create && includes(op.create, f)) { | ||
op.create = without(op.create, f); | ||
op.truncate = op.truncate || []; | ||
op.truncate.push(o); | ||
} else if (!noDelete.has(o)) { | ||
op.delete.push(o); | ||
} | ||
} else if (op.create && includes(op.create, f)) { | ||
op.create = without(op.create, f); | ||
} else if (op.copy && op.copy[f]) { | ||
delete op.copy[f]; | ||
} else { | ||
op.delete.push(f); | ||
} | ||
if (op.rename && op.rename[f]) { | ||
f = op.rename[f]; | ||
} | ||
if (op.edit) { delete op.edit[f]; } | ||
if (op.sel) { delete op.sel[f]; } | ||
if (isFilePath(f)) { | ||
const sb = fileToBufferPath(f); | ||
if (op.save && includes(op.save, sb)) { | ||
op.save = without(op.save, sb); | ||
} | ||
} | ||
} | ||
op.delete = ab; | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const composeTruncate = (op: WorkspaceOp, b: string[] | undefined): void => { | ||
op.truncate = op.truncate || []; | ||
if (emptyArray(b)) { return; } | ||
for (let f of b) { | ||
const [exists, known] = opExists(op, f); | ||
if (known && !exists) { | ||
throw new Error(`truncate: file ${f} does not exist`); | ||
} | ||
if (op.rename && op.rename[f]) { | ||
f = op.rename[f]; | ||
} | ||
op.truncate.push(f); | ||
if (op.edit) { delete op.edit[f]; } | ||
if (op.sel) { delete op.sel[f]; } | ||
if (isFilePath(f)) { | ||
const sb = fileToBufferPath(f); | ||
if (op.save && includes(op.save, sb)) { | ||
op.save = without(op.save, sb); | ||
} | ||
} | ||
} | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const composeEdit = (op: WorkspaceOp, b: FileEdits | undefined): void => { | ||
if (emptyEdit(b)) { return; } | ||
const ab = op.edit || {}; | ||
for (const fb in b) { | ||
if (b.hasOwnProperty(fb)) { | ||
ab[fb] = composeEdits(ab[fb] || [], b[fb]); | ||
op.edit = op.edit || {}; | ||
for (const f of Object.keys(b)) { | ||
const [exists, known] = opExists(op, f); | ||
if (known && !exists) { | ||
throw new Error(`edit: file ${f} does not exist`); | ||
} | ||
op.edit[f] = composeEdits(op.edit[f] || [], b[f]); | ||
if (op.create && includes(op.create, f)) { | ||
const {ret: ret} = countEdits(op.edit[f]); | ||
if (ret !== 0) { | ||
throw new Error(`newly created file has nonzero retain count ${f}`); | ||
} | ||
} | ||
} | ||
op.edit = ab; | ||
}; | ||
@@ -226,9 +552,53 @@ | ||
composeCopy(op, b); | ||
composeRename(op, b); | ||
composeSave(op, b.save); | ||
composeCreate(op, b.create); | ||
composeDelete(op, b.delete); | ||
composeTruncate(op, b.truncate); | ||
composeEdit(op, b.edit); | ||
composeSel(op, b.sel); | ||
op.head = b.head || a.head; | ||
// Simplify. | ||
if (op.copy) { | ||
for (const d of Object.keys(op.copy)) { | ||
const s = op.copy[d]; | ||
if (d === s) { | ||
delete op.copy[d]; | ||
} | ||
} | ||
} | ||
if (op.rename) { | ||
for (const s of Object.keys(op.rename)) { | ||
const d = op.rename[s]; | ||
if (s === d) { | ||
delete op.rename[s]; | ||
} | ||
} | ||
} | ||
if (op.truncate) { | ||
const toRemove: string[] = []; | ||
for (const f of op.truncate) { | ||
if (op.create && includes(op.create, f)) { | ||
toRemove.push(f); | ||
} | ||
} | ||
for (const r of toRemove) { | ||
op.truncate = without(op.truncate, r); | ||
} | ||
} | ||
if (op.save) { | ||
const toRemove: string[] = []; | ||
for (const s of op.save) { | ||
const d = bufferToFilePath(s); | ||
if (op.delete && includes(op.delete, d)) { | ||
toRemove.push(s); | ||
} | ||
} | ||
for (const r of toRemove) { | ||
op.save = without(op.save, r); | ||
} | ||
} | ||
return normalize(op); | ||
} | ||
}; | ||
@@ -285,3 +655,162 @@ export function composeAll(ops: WorkspaceOp[]): WorkspaceOp { | ||
const yCopyFrom = invertBy(y.copy || {}) as { [key: string]: string[] }; | ||
const newFileSrc = {} as { [key: string]: string }; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const transformCopy = (x: WorkspaceOp, y: WorkspaceOp, z: WorkspaceOp): void => { | ||
if (!x.copy) { x.copy = {}; } | ||
if (!y.copy) { y.copy = {}; } | ||
for (const d of Object.keys(x.copy)) { | ||
const s = x.copy[d]; | ||
if (y.copy[d] && y.copy[d] === s) { | ||
continue; | ||
} else if (y.copy[d] && y.copy[d] !== s) { | ||
throw new Error(`copy to ${s}: confilict: ${y.copy[d]}`); | ||
} | ||
const [srcExisted, srcKnown] = opExisted(y, s); | ||
if (srcKnown && !srcExisted) { | ||
throw new Error(`copy: ${s} does not exist`); | ||
} | ||
const [destExists, destKnown] = opExists(y, d); | ||
if (y.rename && y.rename[s] === d) { | ||
continue; | ||
} else if (destExists && destKnown) { | ||
throw new Error(`copy: file ${d} already exists`); | ||
} | ||
z.copy = z.copy || {}; | ||
z.copy[d] = s; | ||
newFileSrc[d] = s; | ||
} | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const transformRename = (x: WorkspaceOp, y: WorkspaceOp, z: WorkspaceOp): void => { | ||
if (!x.rename) { x.rename = {}; } | ||
if (!y.rename) { y.rename = {}; } | ||
for (const s of Object.keys(x.rename)) { | ||
const d = x.rename[s]; | ||
if (y.rename[d] && y.rename[d] === s) { | ||
continue; | ||
} else if (y.rename[d] && y.rename[d] !== s) { | ||
throw new Error(`rename to: ${s} conflict: ${y.rename[d]}`); | ||
} | ||
const [srcExisted, srcKnown] = opExisted(y, s); | ||
if (!srcExisted && srcKnown) { | ||
throw new Error(`rename from: ${d} does not exist`); | ||
} | ||
if (y.rename[s] && y.rename[s] === d) { | ||
continue; | ||
} else if (y.rename[s]) { | ||
z.copy = z.copy || {}; | ||
z.copy[d] = y.rename[s]; | ||
newFileSrc[y.rename[s]] = s; | ||
continue; | ||
} | ||
const [srcExists] = opExists(y, s); | ||
if (srcKnown && !srcExists) { | ||
throw new Error(`rename from: ${s} does not exist`); | ||
} | ||
const [destExisted, destKnown] = opExisted(y, d); | ||
const [destExists, destKnown2] = opExists(y, d); | ||
if (y.copy && y.copy[d] && y.copy[d] === s) { | ||
z.delete = z.delete || []; | ||
z.delete.push(s); | ||
continue; | ||
} else if (destExisted && destKnown) { | ||
throw new Error(`rename to: ${d} already exists`); | ||
} else if (destExists && destKnown2) { | ||
throw new Error(`rename to: ${d} already exists`); | ||
} | ||
z.rename = z.rename || {}; | ||
z.rename[s] = d; | ||
newFileSrc[d] = s; | ||
} | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const transformSave = (x: WorkspaceOp, y: WorkspaceOp, z: WorkspaceOp): void => { | ||
if (!x.save) { x.save = []; } | ||
if (!y.save) { y.save = []; } | ||
z.save = z.save || []; | ||
for (const s of x.save) { | ||
if (y.save && includes(y.save, s)) { | ||
continue; | ||
} | ||
z.save.push(s); | ||
} | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const transformCreate = (x: WorkspaceOp, y: WorkspaceOp, z: WorkspaceOp): void => { | ||
if (!x.create) { x.create = []; } | ||
if (!y.create) { y.create = []; } | ||
z.create = z.create || []; | ||
for (const f of x.create) { | ||
if (y.create && !includes(y.create, f)) { | ||
const [exists, known] = opExists(y, f); | ||
if (known && exists) { | ||
throw new Error(`create: file ${f} already exists`); | ||
} | ||
z.create.push(f); | ||
} | ||
if (y.delete && includes(y.delete, f)) { | ||
throw new Error(`create: conflict: y delete (file ${f})`); | ||
} | ||
} | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const transformDelete = (x: WorkspaceOp, y: WorkspaceOp, z: WorkspaceOp): void => { | ||
if (!x.delete) { x.delete = []; } | ||
if (!y.delete) { y.delete = []; } | ||
z.delete = z.delete || []; | ||
for (const f of x.delete) { | ||
if (y.delete && !includes(y.delete, f)) { | ||
const [exists, known] = opExists(y, f); | ||
if (known && !exists) { | ||
throw new Error(`delete: file ${f} does not exist`); | ||
} | ||
z.delete.push(f); | ||
} | ||
if (y.create && includes(y.create, f)) { | ||
throw new Error(`delete: conflict: y create (file ${f})`); | ||
} | ||
} | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const transformTruncate = (x: WorkspaceOp, y: WorkspaceOp, z: WorkspaceOp): void => { | ||
if (!x.truncate) { x.truncate = []; } | ||
if (!y.truncate) { y.truncate = []; } | ||
z.truncate = z.truncate || []; | ||
for (const f of x.truncate) { | ||
const ycreated = Boolean(y.create && includes(y.create, f)); | ||
const ydeleted = Boolean(y.delete && includes(y.delete, f)); | ||
const ytruncated = includes(y.truncate, f); | ||
const sameTruncateEdit = Boolean(ytruncated && y.edit && x.edit && orderedStringify(y.edit[f]) === orderedStringify(x.edit[f])); | ||
if (y.edit && Object.keys(y.edit[f]).length > 0 && !sameTruncateEdit) { | ||
throw new Error(`truncate: conflict: edit and y truncate (file ${f})`); | ||
} | ||
if (ydeleted) { | ||
throw new Error(`truncate: conflict: y delete (file ${f})`); | ||
} | ||
if (!ytruncated && !ycreated && !ydeleted && !sameTruncateEdit) { | ||
const [exists, known] = opExists(y, f); | ||
if (known && !exists) { | ||
throw new Error(`truncate: ${f} does not exist`); | ||
} | ||
z.truncate.push(f); | ||
} | ||
} | ||
}; | ||
// tslint:disable-next-line no-shadowed-variable | ||
const transformEdit = (x: WorkspaceOp, y: WorkspaceOp, z: WorkspaceOp): void => { | ||
@@ -291,19 +820,73 @@ if (!x.edit) { x.edit = {}; } | ||
const ab: FileEdits = copyHACK(x.edit); | ||
for (const f in x.edit) { | ||
z.edit = z.edit || {}; | ||
for (let f in x.edit) { | ||
if (x.edit.hasOwnProperty(f)) { | ||
const xedit = x.edit[f]; | ||
const yedit = y.edit[f]; | ||
let edit: EditOps | undefined; | ||
if (yedit) { | ||
edit = transformEditOps(copyHACK(x.edit[f]), copyHACK(yedit), primary); | ||
let edit = x.edit[f]; | ||
if (y.rename && y.rename[f]) { | ||
const [exists, known] = opExists(y, y.rename[f]); | ||
if (known && !exists) { | ||
throw new Error(`edit: file ${f} does not exist`); | ||
} | ||
} else { | ||
const [exists, known] = opExists(y, f); | ||
if (known && !exists) { | ||
throw new Error(`edit: file ${f} does not exist`); | ||
} | ||
} | ||
if (JSON.stringify(xedit) !== JSON.stringify(yedit)) { | ||
if (edit) { ab[f] = edit!; } | ||
if (yCopyFrom[f]) { | ||
for (const d of yCopyFrom[f]) { | ||
z.edit[d] = transformEditOps(copyHACK(x.edit[f] || []), copyHACK(y.edit[d] || []), primary); | ||
} | ||
} | ||
let yf: string; | ||
if (newFileSrc[f]) { | ||
yf = newFileSrc[f]; | ||
} else { | ||
delete ab[f]; | ||
yf = f; | ||
} | ||
if (y.rename && y.rename[yf]) { | ||
const d = y.rename[yf]; | ||
f = d; | ||
edit = transformEditOps(copyHACK(x.edit[f] || []), copyHACK(y.edit[d] || []), primary); | ||
} | ||
if (y.edit[yf]) { | ||
edit = transformEditOps(copyHACK(edit || []), copyHACK(y.edit[yf]), primary); | ||
} | ||
if (JSON.stringify(xedit) !== JSON.stringify(yedit)) { | ||
if (z.edit[f] && Object.keys(z.edit[f]).length !== 0) { | ||
throw new Error(`copy/rename edit conflict`); | ||
} | ||
if (edit) { z.edit[f] = edit!; } | ||
} | ||
} | ||
} | ||
z.edit = ab; | ||
for (let f in x.edit) { | ||
if (isBufferPath(f)) { | ||
const d = bufferToFilePath(f); | ||
if (y.save && includes(y.save, f)) { | ||
z.edit[d] = transformEditOps(copyHACK(x.edit[f] || []), copyHACK(y.edit[d] || []), primary); | ||
} | ||
if (x.save && includes(x.save, f)) { | ||
z.edit[d] = y.edit[d]; | ||
} | ||
} | ||
} | ||
for (let f in x.edit) { | ||
if (isFilePath(f)) { | ||
const s = fileToBufferPath(f); | ||
if (x.save && includes(x.save, s)) { | ||
z.edit[f] = transformEditOps(copyHACK(x.edit[f] || []), copyHACK(y.edit[s] || []), primary); | ||
const z1 = transformEditOps(copyHACK(z.edit[f] || []), copyHACK(y.edit[f] || []), primary); | ||
z.edit[f] = composeEdits(copyHACK(y.edit[f] || []), z1); | ||
} | ||
} | ||
} | ||
}; | ||
@@ -330,2 +913,8 @@ | ||
const z: WorkspaceOp = {}; | ||
transformCopy(x, y, z); | ||
transformRename(x, y, z); | ||
transformSave(x, y, z); | ||
transformCreate(x, y, z); | ||
transformDelete(x, y, z); | ||
transformTruncate(x, y, z); | ||
transformEdit(x, y, z); | ||
@@ -332,0 +921,0 @@ transformSel(x, y, z); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
556097
77
16788
4
9
+ Addedlodash@^4.17.2
+ Addedlodash@4.17.21(transitive)