diff --git a/.gitignore b/.gitignore index d48669b2..843bd5fa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules build .DS_Store .clang_complete + +/browser.js +emsdk_portable diff --git a/.npmignore b/.npmignore index 78a84fd8..9ecf6428 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,4 @@ test build .gitignore +emsdk_portable diff --git a/.travis.yml b/.travis.yml index 4b8399c3..bdce12cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: node_js +sudo: false notifications: email: @@ -8,8 +9,17 @@ notifications: node_js: - "node" +before_install: + - export CXX="g++-4.8" CC="gcc-4.8" + - curl https://cmake.org/files/v3.6/cmake-3.6.3-Linux-x86_64.tar.gz | tar xz + - export PATH=${PWD}/cmake-3.6.3-Linux-x86_64/bin:$PATH + - script/install-emscripten.sh + script: - - "npm run ci" + - npm run standard + - npm run build:browser + - npm run test:browser + - npm run test:node git: depth: 10 @@ -18,7 +28,15 @@ branches: only: - master -sudo: false +cache: + directories: + - emsdk_portable + - $HOME/.emscripten_cache -env: - - CXX=clang++ +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.8 + - g++-4.8 diff --git a/index.js b/index.js index a2356884..418d695d 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,9 @@ -try { - module.exports = require('./build/Release/superstring.node') -} catch (e) { - module.exports = require('./build/Debug/superstring.node') +if (process.env.SUPERSTRING_USE_BROWSER_VERSION) { + module.exports = require('./browser'); +} else { + try { + module.exports = require('./build/Release/superstring.node') + } catch (e) { + module.exports = require('./build/Debug/superstring.node') + } } diff --git a/package.json b/package.json index ad01482a..ed4a5927 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,19 @@ "name": "superstring", "version": "1.1.0", "description": "A data structure to efficiently represent the results of applying patches.", + "main": "./index", + "browser": "./browser", "scripts": { - "test-native": "script/test-native.js", - "test": "mocha test/js/*.js", + "build:node": "node-gyp rebuild", + "build:browser": "script/build-browser-version.sh", + "build": "npm run build:node && npm run build:browser", + "test:native": "script/test-native.js", + "test:node": "mocha test/js/*.js", + "test:browser": "SUPERSTRING_USE_BROWSER_VERSION=1 mocha test/js/*.js", + "test": "npm run test:node && npm run test:browser", "benchmark": "node benchmark/marker-index.benchmark.js", - "prepublish": "npm run standard", - "standard": "standard --recursive src test", - "ci": "npm run standard && node-gyp rebuild --debug --tests && npm run test" + "prepublish": "not-in-install && npm run build:browser || in-install", + "standard": "standard --recursive src test" }, "repository": { "type": "git", @@ -29,6 +35,7 @@ }, "devDependencies": { "chai": "^2.0.0", + "in-publish": "^2.0.0", "mocha": "^2.3.4", "random-seed": "^0.2.0", "segfault-handler": "^1.0.0", diff --git a/script/build-browser-version.sh b/script/build-browser-version.sh new file mode 100755 index 00000000..635bffeb --- /dev/null +++ b/script/build-browser-version.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +EM_COMPILER_PATH=$(find emsdk_portable -name em++ | head -n1) + +echo "Running ${EM_COMPILER_PATH}" +${EM_COMPILER_PATH} \ + --bind \ + -o browser.js \ + -O3 \ + -std=c++14 \ + -I src/bindings/em \ + -I src/core \ + --pre-js src/bindings/em/prologue.js \ + --post-js src/bindings/em/epilogue.js \ + src/core/*.cc \ + src/bindings/em/*.cc \ + -s TOTAL_MEMORY=134217728 \ + --memory-init-file 0 \ diff --git a/script/install-emscripten.sh b/script/install-emscripten.sh new file mode 100755 index 00000000..25695ca2 --- /dev/null +++ b/script/install-emscripten.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -e + +EMSCRIPTEN_DOWNLOAD_URL='https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz' +EMSDK_PATH="./emsdk_portable/emsdk" + +if [ ! -f $EMSDK_PATH ]; then + echo 'Downloading emscripten SDK installer...' + curl $EMSCRIPTEN_DOWNLOAD_URL | tar xz +fi + +echo 'Installing emscripten SDK...' + +$EMSDK_PATH update +$EMSDK_PATH install -j4 latest +$EMSDK_PATH activate latest diff --git a/src/bindings/buffer-offset-index-wrapper.cc b/src/bindings/buffer-offset-index-wrapper.cc index 2346f687..112df62c 100644 --- a/src/bindings/buffer-offset-index-wrapper.cc +++ b/src/bindings/buffer-offset-index-wrapper.cc @@ -1,4 +1,5 @@ #include "buffer-offset-index-wrapper.h" +#include "noop.h" #include "point-wrapper.h" using namespace v8; @@ -8,6 +9,7 @@ void BufferOffsetIndexWrapper::init(Local exports) { constructor_template->SetClassName(Nan::New("BufferOffsetIndex").ToLocalChecked()); constructor_template->InstanceTemplate()->SetInternalFieldCount(1); const auto &prototype_template = constructor_template->PrototypeTemplate(); + prototype_template->Set(Nan::New("delete").ToLocalChecked(), Nan::New(noop)); prototype_template->Set(Nan::New("splice").ToLocalChecked(), Nan::New(splice)); prototype_template->Set(Nan::New("positionForCharacterIndex").ToLocalChecked(), Nan::New(position_for_character_index)); prototype_template->Set(Nan::New("characterIndexForPosition").ToLocalChecked(), Nan::New(character_index_for_position)); diff --git a/src/bindings/em/as.h b/src/bindings/em/as.h new file mode 100644 index 00000000..6970f7d4 --- /dev/null +++ b/src/bindings/em/as.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "patch.h" + +template<> inline Patch const * emscripten::val::as(void) const { + using namespace emscripten; + using namespace internal; + + EM_DESTRUCTORS destructors; + EM_GENERIC_WIRE_TYPE result = _emval_as(handle, TypeID>::get(), &destructors); + DestructorsRunner dr(destructors); + return fromGenericWireType(result); +} diff --git a/src/bindings/em/auto-wrap.h b/src/bindings/em/auto-wrap.h new file mode 100644 index 00000000..a7b4a985 --- /dev/null +++ b/src/bindings/em/auto-wrap.h @@ -0,0 +1,349 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "as.h" +#include "flat_set.h" +#include "marker-index.h" +#include "optional.h" +#include "point-wrapper.h" +#include "point.h" +#include "text.h" + +/********** **********/ + +template +struct em_wrap_type_base { + + using LocalType = LocalTypeTpl; + using WireType = WireTypeTpl; + +}; + +template +struct em_wrap_type_simple : public em_wrap_type_base { + + static LocalType receive(WireType data) { return data; } + static WireType transmit(LocalType data) { return data; } + +}; + +/********** **********/ + +template +struct em_wrap_type : public em_wrap_type_simple {}; + +/********** **********/ + +template +LocalType em_receive(typename em_wrap_type::type>::WireType data) +{ + return em_wrap_type::type>::receive(data); +} + +template +typename em_wrap_type::type>::WireType em_transmit(LocalType data) +{ + return em_wrap_type::type>::transmit(data); +} + +/********** **********/ + +template <> +struct em_wrap_type : public em_wrap_type_base {}; + +template <> +struct em_wrap_type : public em_wrap_type_simple {}; + +template +struct em_wrap_type> : public em_wrap_type_base, emscripten::val> { + + static std::vector receive(emscripten::val const & val) + { + std::vector vec; + + for (auto t = 0u, T = val["length"].as(); t < T; ++t) + vec.push_back(val[t].as()); + + return vec; + } + + static emscripten::val transmit(std::vector const & vec) + { + auto array = emscripten::val::array(); + + for (auto const & t : vec) + array.call("push", em_transmit(t)); + + return array; + } + +}; + +template +struct em_wrap_type> : public em_wrap_type_base, emscripten::val> { + + static optional receive(emscripten::val const & val) + { + if (!val.as()) + return optional(); + + return optional(val.as()); + } + + static emscripten::val transmit(optional const & opt) + { + if (!opt) + return emscripten::val::undefined(); + + return emscripten::val(em_transmit(*opt)); + } + +}; + +template +struct em_wrap_type> : public em_wrap_type_base, emscripten::val> { + + static std::unordered_map receive(emscripten::val const & val) + { + throw std::runtime_error("Unimplemented"); + } + + static emscripten::val transmit(std::unordered_map const & map) + { + auto object = emscripten::val::object(); + + for (auto const & t : map) + object.set(em_transmit(t.first), em_transmit(t.second)); + + return object; + } + +}; + +template +struct em_wrap_type> : public em_wrap_type_base, emscripten::val> { + + static flat_set receive(emscripten::val const & val) + { + throw std::runtime_error("Unimplemented"); + } + + static emscripten::val transmit(flat_set const & set) + { + auto object = emscripten::val::global("Set").new_(); + + for (auto const & t : set) + object.call("add", em_transmit(t)); + + return object; + } + +}; + +template <> +struct em_wrap_type : public em_wrap_type_base { + + static MarkerIndex::SpliceResult receive(emscripten::val const & val) + { + throw std::runtime_error("Unimplemented"); + } + + static emscripten::val transmit(MarkerIndex::SpliceResult const & spliceResult) + { + auto object = emscripten::val::object(); + + object.set("touch", em_transmit(spliceResult.touch)); + object.set("inside", em_transmit(spliceResult.inside)); + object.set("overlap", em_transmit(spliceResult.overlap)); + object.set("surround", em_transmit(spliceResult.surround)); + + return object; + } + +}; + +template <> +struct em_wrap_type : public em_wrap_type_base { + + static Text receive(std::string const & str) + { + return Text(str.begin(), str.end()); + } + + static std::string transmit(Text const & text) + { + return std::string(text.begin(), text.end()); + } + +}; + +template <> +struct em_wrap_type> : public em_wrap_type_base, emscripten::val> { + + static std::unique_ptr receive(emscripten::val const & val) + { + return std::make_unique(em_wrap_type::receive(val.as())); + } + + static emscripten::val transmit(std::unique_ptr const & text) + { + if (!text) + return emscripten::val::undefined(); + + return emscripten::val(em_wrap_type::transmit(*text)); + } + +}; + +template <> +struct em_wrap_type : public em_wrap_type_base { + + static Text * receive(emscripten::val const & val) + { + return new Text(em_wrap_type::receive(val.as())); + } + + static emscripten::val transmit(Text * text) + { + if (!text) + return emscripten::val::undefined(); + + return emscripten::val(em_wrap_type::transmit(*text)); + } + +}; + +/********** **********/ + +template +struct em_wrap_fn; + +template +struct em_wrap_fn +{ + static void wrap(T & t, typename em_wrap_type::type>::WireType ... args) + { + return (t.*fn)(em_wrap_type::type>::receive(args) ...); + } +}; + +template +struct em_wrap_fn +{ + static void wrap(T const & t, typename em_wrap_type::type>::WireType ... args) + { + return (t.*fn)(em_wrap_type::type>::receive(args) ...); + } +}; + +template +struct em_wrap_fn +{ + static typename em_wrap_type::type>::WireType wrap(T & t, typename em_wrap_type::type>::WireType ... args) + { + return em_wrap_type::type>::transmit((t.*fn)(em_wrap_type::type>::receive(args) ...)); + } +}; + +template +struct em_wrap_fn +{ + static typename em_wrap_type::type>::WireType wrap(T const & t, typename em_wrap_type::type>::WireType ... args) + { + return em_wrap_type::type>::transmit((t.*fn)(em_wrap_type::type>::receive(args) ...)); + } +}; + +template +struct em_wrap_fn +{ + static void wrap(T & t, typename em_wrap_type::type>::WireType ... args) + { + return fn(t, em_wrap_type::type>::receive(args) ...); + } +}; + +template +struct em_wrap_fn +{ + static void wrap(T const & t, typename em_wrap_type::type>::WireType ... args) + { + return fn(t, em_wrap_type::type>::receive(args) ...); + } +}; + +template +struct em_wrap_fn +{ + static typename em_wrap_type::type>::WireType wrap(T & t, typename em_wrap_type::type>::WireType ... args) + { + return em_wrap_type::type>::transmit(fn(t, em_wrap_type::type>::receive(args) ...)); + } +}; + +template +struct em_wrap_fn +{ + static typename em_wrap_type::type>::WireType wrap(T const & t, typename em_wrap_type::type>::WireType ... args) + { + return em_wrap_type::type>::transmit(fn(t, em_wrap_type::type>::receive(args) ...)); + } +}; + +/********** **********/ + +template +struct em_wrap_static_fn; + +template +struct em_wrap_static_fn +{ + static void wrap(typename em_wrap_type::type>::WireType ... args) + { + return fn(em_wrap_type::type>::receive(args) ...); + } +}; + +template +struct em_wrap_static_fn +{ + static typename em_wrap_type::type>::WireType wrap(typename em_wrap_type::type>::WireType ... args) + { + return em_wrap_type::type>::transmit(fn(em_wrap_type::type>::receive(args) ...)); + } +}; + +/********** **********/ + +template +struct em_wrap_property; + +template +struct em_wrap_property +{ + static typename em_wrap_type::type>::WireType get(T const & t) + { + return em_transmit(t.*property); + } + + static void set(T & t, typename em_wrap_type::type>::WireType wire) + { + t.*property = em_receive(wire); + } +}; + +/********** **********/ + +#define WRAP(FN) WRAP_OVERLOAD((FN), decltype(FN)) +#define WRAP_OVERLOAD(FN, ...) &em_wrap_fn<__VA_ARGS__, (FN)>::wrap + +#define WRAP_STATIC(FN) WRAP_STATIC_OVERLOAD((FN), decltype(FN)) +#define WRAP_STATIC_OVERLOAD(FN, ...) &em_wrap_static_fn<__VA_ARGS__, (FN)>::wrap + +#define WRAP_FIELD(CLASS, FIELD) &em_wrap_property::get, &em_wrap_property::set diff --git a/src/bindings/em/buffer-offset-index.cc b/src/bindings/em/buffer-offset-index.cc new file mode 100644 index 00000000..a56fc42c --- /dev/null +++ b/src/bindings/em/buffer-offset-index.cc @@ -0,0 +1,19 @@ +#include "auto-wrap.h" +#include "buffer-offset-index.h" + +#include + +EMSCRIPTEN_BINDINGS(BufferOffsetIndex) { + + emscripten::class_("BufferOffsetIndex") + + .constructor<>() + + .function("splice", WRAP(&BufferOffsetIndex::splice)) + + .function("characterIndexForPosition", WRAP(&BufferOffsetIndex::character_index_for_position)) + .function("positionForCharacterIndex", WRAP(&BufferOffsetIndex::position_for_character_index)) + + ; + +} diff --git a/src/bindings/em/epilogue.js b/src/bindings/em/epilogue.js new file mode 100644 index 00000000..b6be84de --- /dev/null +++ b/src/bindings/em/epilogue.js @@ -0,0 +1,2 @@ + return Module; +})); diff --git a/src/bindings/em/marker-index.cc b/src/bindings/em/marker-index.cc new file mode 100644 index 00000000..b17801bf --- /dev/null +++ b/src/bindings/em/marker-index.cc @@ -0,0 +1,48 @@ +#include "auto-wrap.h" +#include "marker-index.h" + +#include + +EMSCRIPTEN_BINDINGS(MarkerIndex) { + + emscripten::class_("MarkerIndex") + + .constructor<>() + .constructor() + + .function("generateRandomNumber", WRAP(&MarkerIndex::generate_random_number)) + + .function("insert", WRAP(&MarkerIndex::insert)) + .function("setExclusive", WRAP(&MarkerIndex::set_exclusive)) + .function("remove", WRAP(&MarkerIndex::remove)) + .function("splice", WRAP(&MarkerIndex::splice)) + + .function("has", WRAP(&MarkerIndex::has)) + .function("getStart", WRAP(&MarkerIndex::get_start)) + .function("getEnd", WRAP(&MarkerIndex::get_end)) + .function("getRange", WRAP(&MarkerIndex::get_range)) + + .function("compare", WRAP(&MarkerIndex::compare)) + + .function("findIntersecting", WRAP(&MarkerIndex::find_intersecting)) + .function("findContaining", WRAP(&MarkerIndex::find_containing)) + .function("findContainedIn", WRAP(&MarkerIndex::find_contained_in)) + .function("findStartingIn", WRAP(&MarkerIndex::find_starting_in)) + .function("findStartingAt", WRAP(&MarkerIndex::find_starting_at)) + .function("findEndingIn", WRAP(&MarkerIndex::find_ending_in)) + .function("findEndingAt", WRAP(&MarkerIndex::find_ending_at)) + + .function("dump", WRAP(&MarkerIndex::dump)) + + ; + + emscripten::value_object("SpliceResult") + + .field("touch", &MarkerIndex::SpliceResult::touch) + .field("inside", &MarkerIndex::SpliceResult::inside) + .field("overlap", &MarkerIndex::SpliceResult::overlap) + .field("surround", &MarkerIndex::SpliceResult::surround) + + ; + +} diff --git a/src/bindings/em/patch.cc b/src/bindings/em/patch.cc new file mode 100644 index 00000000..5a94f9f2 --- /dev/null +++ b/src/bindings/em/patch.cc @@ -0,0 +1,101 @@ +#include +#include + +#include "as.h" +#include "auto-wrap.h" +#include "patch.h" + +#include +#include + +Patch * constructor(emscripten::val val) +{ + bool merge_adjacent_hunks = false; + + if (val.as() && val["mergeAdjacentHunks"].as()) + merge_adjacent_hunks = true; + + return new Patch(merge_adjacent_hunks); +} + +std::vector serialize(Patch const & patch) +{ + std::vector vec; + patch.serialize(&vec); + + return vec; +} + +Patch * compose(std::vector const & vec) +{ + return new Patch(vec); +} + +Patch * deserialize(std::vector const & vec) +{ + return new Patch(vec); +} + +Point get_old_extent(Patch::Hunk const & hunk) +{ + return hunk.old_end.traversal(hunk.old_start); +} + +Point get_new_extent(Patch::Hunk const & hunk) +{ + return hunk.new_end.traversal(hunk.new_start); +} + +template +void hunk_set_noop(Patch::Hunk & hunk, T const &) +{ +} + +EMSCRIPTEN_BINDINGS(Patch) { + + emscripten::class_("Patch") + + .constructor<>() + .constructor(WRAP_STATIC(&constructor), emscripten::allow_raw_pointers()) + + .function("splice", WRAP_OVERLOAD(&Patch::splice, bool (Patch::*)(Point, Point, Point))) + .function("splice", WRAP_OVERLOAD(&Patch::splice, bool (Patch::*)(Point, Point, Point, std::unique_ptr, std::unique_ptr))) + + .function("spliceOld", WRAP(&Patch::splice_old)) + + .function("copy", WRAP(&Patch::copy)) + .function("invert", WRAP(&Patch::invert)) + + .function("getHunks", WRAP(&Patch::get_hunks)) + .function("getHunksInNewRange", WRAP_OVERLOAD(&Patch::get_hunks_in_new_range, std::vector (Patch::*)(Point, Point))) + .function("getHunksInNewRange", WRAP_OVERLOAD(&Patch::get_hunks_in_new_range, std::vector (Patch::*)(Point, Point, bool))) + .function("getHunksInOldRange", WRAP(&Patch::get_hunks_in_old_range)) + .function("getHunkCount", WRAP(&Patch::get_hunk_count)) + + .function("hunkForOldPosition", WRAP(&Patch::hunk_for_old_position)) + .function("hunkForNewPosition", WRAP(&Patch::hunk_for_new_position)) + + .function("rebalance", WRAP(&Patch::rebalance)) + + .function("serialize", WRAP(&serialize)) + + .class_function("compose", WRAP_STATIC(&compose), emscripten::allow_raw_pointers()) + + .class_function("deserialize", WRAP_STATIC(&deserialize), emscripten::allow_raw_pointers()) + + ; + + emscripten::value_object("Hunk") + + .field("oldStart", WRAP_FIELD(Patch::Hunk, old_start)) + .field("oldEnd", WRAP_FIELD(Patch::Hunk, old_end)) + + .field("newStart", WRAP_FIELD(Patch::Hunk, new_start)) + .field("newEnd", WRAP_FIELD(Patch::Hunk, new_end)) + + .field("oldText", WRAP_FIELD(Patch::Hunk, old_text)) + .field("newText", WRAP_FIELD(Patch::Hunk, new_text)) + + ; + +} diff --git a/src/bindings/em/point-wrapper.h b/src/bindings/em/point-wrapper.h new file mode 100644 index 00000000..0fe2a8fc --- /dev/null +++ b/src/bindings/em/point-wrapper.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include "point.h" + +struct PointWrapper { + + double row; + double column; + + PointWrapper(void) + : row(0) + , column(0) + { + } + + PointWrapper(Point const & point) + : row(point.row) + , column(point.column) + { + } + + operator Point(void) const + { + unsigned row = std::min(this->row, static_cast(std::numeric_limits::max())); + unsigned column = std::min(this->column, static_cast(std::numeric_limits::max())); + + return Point(row, column); + } + +}; diff --git a/src/bindings/em/point.cc b/src/bindings/em/point.cc new file mode 100644 index 00000000..4c6f033d --- /dev/null +++ b/src/bindings/em/point.cc @@ -0,0 +1,16 @@ +#include "auto-wrap.h" +#include "point-wrapper.h" +#include "point.h" + +#include + +EMSCRIPTEN_BINDINGS(Point) { + + emscripten::value_object("Point") + + .field("row", WRAP_FIELD(PointWrapper, row)) + .field("column", WRAP_FIELD(PointWrapper, column)) + + ; + +} diff --git a/src/bindings/em/prologue.js b/src/bindings/em/prologue.js new file mode 100644 index 00000000..e3f7da8b --- /dev/null +++ b/src/bindings/em/prologue.js @@ -0,0 +1,9 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + window.Superstring = factory(); + } +}(this, function () { diff --git a/src/bindings/em/range.cc b/src/bindings/em/range.cc new file mode 100644 index 00000000..6f120273 --- /dev/null +++ b/src/bindings/em/range.cc @@ -0,0 +1,15 @@ +#include "auto-wrap.h" +#include "range.h" + +#include + +EMSCRIPTEN_BINDINGS(Range) { + + emscripten::value_object("Range") + + .field("start", WRAP_FIELD(Range, start)) + .field("end", WRAP_FIELD(Range, end)) + + ; + +} diff --git a/src/bindings/marker-index-wrapper.cc b/src/bindings/marker-index-wrapper.cc index 2bb6f41e..b8e78e48 100644 --- a/src/bindings/marker-index-wrapper.cc +++ b/src/bindings/marker-index-wrapper.cc @@ -2,6 +2,7 @@ #include #include "marker-index.h" #include "nan.h" +#include "noop.h" #include "optional.h" #include "point-wrapper.h" #include "range.h" @@ -23,11 +24,12 @@ void MarkerIndexWrapper::init(Local exports) { const auto &prototype_template = constructor_template->PrototypeTemplate(); + prototype_template->Set(Nan::New("delete").ToLocalChecked(), Nan::New(noop)); prototype_template->Set(Nan::New("generateRandomNumber").ToLocalChecked(), Nan::New(generate_random_number)); prototype_template->Set(Nan::New("insert").ToLocalChecked(), Nan::New(insert)); prototype_template->Set(Nan::New("setExclusive").ToLocalChecked(), Nan::New(set_exclusive)); - prototype_template->Set(Nan::New("delete").ToLocalChecked(), Nan::New(delete_marker)); + prototype_template->Set(Nan::New("remove").ToLocalChecked(), Nan::New(remove)); prototype_template->Set(Nan::New("has").ToLocalChecked(), Nan::New(has)); prototype_template->Set(Nan::New("splice").ToLocalChecked(), Nan::New(splice)); prototype_template->Set(Nan::New("getStart").ToLocalChecked(), Nan::New(get_start)); @@ -146,12 +148,12 @@ void MarkerIndexWrapper::set_exclusive(const Nan::FunctionCallbackInfo &i } } -void MarkerIndexWrapper::delete_marker(const Nan::FunctionCallbackInfo &info) { +void MarkerIndexWrapper::remove(const Nan::FunctionCallbackInfo &info) { MarkerIndexWrapper *wrapper = Nan::ObjectWrap::Unwrap(info.This()); optional id = marker_id_from_js(info[0]); if (id) { - wrapper->marker_index.delete_marker(*id); + wrapper->marker_index.remove(*id); } } @@ -210,9 +212,9 @@ void MarkerIndexWrapper::get_range(const Nan::FunctionCallbackInfo &info) optional id = marker_id_from_js(info[0]); if (id) { Range range = wrapper->marker_index.get_range(*id); - auto result = Nan::New(2); - result->Set(0, PointWrapper::from_point(range.start)); - result->Set(1, PointWrapper::from_point(range.end)); + auto result = Nan::New(); + result->Set(Nan::New(start_string), PointWrapper::from_point(range.start)); + result->Set(Nan::New(end_string), PointWrapper::from_point(range.end)); info.GetReturnValue().Set(result); } } diff --git a/src/bindings/marker-index-wrapper.h b/src/bindings/marker-index-wrapper.h index e851fad0..df799912 100644 --- a/src/bindings/marker-index-wrapper.h +++ b/src/bindings/marker-index-wrapper.h @@ -18,7 +18,7 @@ class MarkerIndexWrapper : public Nan::ObjectWrap { static optional bool_from_js(v8::Local value); static void insert(const Nan::FunctionCallbackInfo &info); static void set_exclusive(const Nan::FunctionCallbackInfo &info); - static void delete_marker(const Nan::FunctionCallbackInfo &info); + static void remove(const Nan::FunctionCallbackInfo &info); static void has(const Nan::FunctionCallbackInfo &info); static void splice(const Nan::FunctionCallbackInfo &info); static void get_start(const Nan::FunctionCallbackInfo &info); diff --git a/src/bindings/noop.h b/src/bindings/noop.h new file mode 100644 index 00000000..6b446a5a --- /dev/null +++ b/src/bindings/noop.h @@ -0,0 +1,5 @@ +#pragma once + +#include "nan.h" + +static void noop(const Nan::FunctionCallbackInfo&) {} diff --git a/src/bindings/patch-wrapper.cc b/src/bindings/patch-wrapper.cc index 3a168004..50bc112c 100644 --- a/src/bindings/patch-wrapper.cc +++ b/src/bindings/patch-wrapper.cc @@ -1,3 +1,4 @@ +#include "noop.h" #include "patch-wrapper.h" #include #include @@ -46,14 +47,6 @@ class HunkWrapper : public Nan::ObjectWrap { Nan::SetAccessor(instance_template, Nan::New("oldEnd").ToLocalChecked(), get_old_end); Nan::SetAccessor(instance_template, Nan::New("newEnd").ToLocalChecked(), get_new_end); - // Non-enumerable legacy properties for backward compatibility - Nan::SetAccessor(instance_template, Nan::New("start").ToLocalChecked(), get_new_start, nullptr, Handle(), - AccessControl::DEFAULT, PropertyAttribute::DontEnum); - Nan::SetAccessor(instance_template, Nan::New("oldExtent").ToLocalChecked(), get_old_extent, nullptr, Handle(), - AccessControl::DEFAULT, PropertyAttribute::DontEnum); - Nan::SetAccessor(instance_template, Nan::New("newExtent").ToLocalChecked(), get_new_extent, nullptr, Handle(), - AccessControl::DEFAULT, PropertyAttribute::DontEnum); - const auto &prototype_template = constructor_template->PrototypeTemplate(); prototype_template->Set(Nan::New("toString").ToLocalChecked(), Nan::New(to_string)); hunk_wrapper_constructor.Reset(constructor_template->GetFunction()); @@ -100,16 +93,6 @@ class HunkWrapper : public Nan::ObjectWrap { info.GetReturnValue().Set(PointWrapper::from_point(hunk.new_end)); } - static void get_old_extent(v8::Local property, const Nan::PropertyCallbackInfo &info) { - Patch::Hunk &hunk = Nan::ObjectWrap::Unwrap(info.This())->hunk; - info.GetReturnValue().Set(PointWrapper::from_point(hunk.old_end.traversal(hunk.old_start))); - } - - static void get_new_extent(v8::Local property, const Nan::PropertyCallbackInfo &info) { - Patch::Hunk &hunk = Nan::ObjectWrap::Unwrap(info.This())->hunk; - info.GetReturnValue().Set(PointWrapper::from_point(hunk.new_end.traversal(hunk.new_start))); - } - static void to_string(const Nan::FunctionCallbackInfo &info) { Patch::Hunk &hunk = Nan::ObjectWrap::Unwrap(info.This())->hunk; std::stringstream result; @@ -129,6 +112,7 @@ void PatchWrapper::init(Local exports) { constructor_template_local->Set(Nan::New("compose").ToLocalChecked(), Nan::New(compose)); constructor_template_local->InstanceTemplate()->SetInternalFieldCount(1); const auto &prototype_template = constructor_template_local->PrototypeTemplate(); + prototype_template->Set(Nan::New("delete").ToLocalChecked(), Nan::New(noop)); prototype_template->Set(Nan::New("splice").ToLocalChecked(), Nan::New(splice)); prototype_template->Set(Nan::New("spliceOld").ToLocalChecked(), Nan::New(splice_old)); prototype_template->Set(Nan::New("copy").ToLocalChecked(), Nan::New(copy)); diff --git a/src/core/buffer-offset-index.cc b/src/core/buffer-offset-index.cc index abdd628f..96ba6147 100644 --- a/src/core/buffer-offset-index.cc +++ b/src/core/buffer-offset-index.cc @@ -54,7 +54,7 @@ BufferOffsetIndex::~BufferOffsetIndex() { } } -void BufferOffsetIndex::splice(unsigned start_row, unsigned deleted_lines_count, std::vector &new_line_lengths) { +void BufferOffsetIndex::splice(unsigned start_row, unsigned deleted_lines_count, std::vector const &new_line_lengths) { auto start_node = find_and_bubble_node_up_to_root(start_row - 1); auto end_node = find_and_bubble_node_up_to_root(start_row + deleted_lines_count); @@ -251,7 +251,7 @@ void BufferOffsetIndex::rotate_node_right(LineNode * pivot, LineNode * root, Lin pivot->compute_subtree_extents(); } -LineNode *BufferOffsetIndex::build_node_tree_from_line_lengths(std::vector &line_lengths, unsigned start, unsigned end, unsigned min_priority) { +LineNode *BufferOffsetIndex::build_node_tree_from_line_lengths(std::vector const &line_lengths, unsigned start, unsigned end, unsigned min_priority) { if (start == end) { return nullptr; } else { diff --git a/src/core/buffer-offset-index.h b/src/core/buffer-offset-index.h index d3087115..7d0c747a 100644 --- a/src/core/buffer-offset-index.h +++ b/src/core/buffer-offset-index.h @@ -11,7 +11,7 @@ class BufferOffsetIndex { public: BufferOffsetIndex(); ~BufferOffsetIndex(); - void splice(unsigned, unsigned, std::vector&); + void splice(unsigned, unsigned, std::vector const&); unsigned character_index_for_position(Point) const; Point position_for_character_index(unsigned) const; @@ -20,7 +20,7 @@ class BufferOffsetIndex { void bubble_node_down(LineNode *, LineNode *); void rotate_node_left(LineNode *, LineNode *, LineNode *); void rotate_node_right(LineNode *, LineNode *, LineNode *); - LineNode *build_node_tree_from_line_lengths(std::vector&, unsigned, unsigned, unsigned); + LineNode *build_node_tree_from_line_lengths(std::vector const&, unsigned, unsigned, unsigned); LineNode *root; std::default_random_engine rng_engine; diff --git a/src/core/marker-index.cc b/src/core/marker-index.cc index 66ce3b7b..444166ba 100644 --- a/src/core/marker-index.cc +++ b/src/core/marker-index.cc @@ -404,7 +404,7 @@ void MarkerIndex::set_exclusive(MarkerId id, bool exclusive) { } } -void MarkerIndex::delete_marker(MarkerId id) { +void MarkerIndex::remove(MarkerId id) { Node *start_node = start_nodes_by_id.find(id)->second; Node *end_node = end_nodes_by_id.find(id)->second; diff --git a/src/core/marker-index.h b/src/core/marker-index.h index 7a6a5f44..7b51fbce 100644 --- a/src/core/marker-index.h +++ b/src/core/marker-index.h @@ -19,12 +19,12 @@ class MarkerIndex { flat_set surround; }; - MarkerIndex(unsigned seed); + MarkerIndex(unsigned seed = 0u); ~MarkerIndex(); int generate_random_number(); void insert(MarkerId id, Point start, Point end); void set_exclusive(MarkerId id, bool exclusive); - void delete_marker(MarkerId id); + void remove(MarkerId id); bool has(MarkerId id); SpliceResult splice(Point start, Point old_extent, Point new_extent); Point get_start(MarkerId id) const; diff --git a/src/core/patch.cc b/src/core/patch.cc index 4345c660..b3a35288 100644 --- a/src/core/patch.cc +++ b/src/core/patch.cc @@ -1249,37 +1249,33 @@ static const uint32_t SERIALIZATION_VERSION = 1; enum Transition : uint32_t { None, Left, Right, Up }; -template T network_to_host(T input); +template +void append_to_buffer(vector *output, T value) { + for (auto t = 0u; t < sizeof(T); ++t) { + output->push_back(value & 0xFF); + value >>= 8; + } +} -template T host_to_network(T input); +template +T get_from_buffer(const uint8_t **data, const uint8_t *end) { -template <> uint16_t network_to_host(uint16_t input) { return ntohs(input); } + // Note: We can't optimize this function by casting the data argument into a T*, + // because it would only work on architectures that support reading from unaligned + // memory. It would work on X86, but not on asm.js and possibly other architectures. -template <> uint16_t host_to_network(uint16_t input) { return htons(input); } + T value = 0; -template <> uint32_t network_to_host(uint32_t input) { return ntohl(input); } + if (static_cast(end - *data) >= sizeof(T)) + for (auto t = 0u; t < sizeof(T); ++t) + value |= static_cast(*((*data)++)) << static_cast(8 * t); -template <> uint32_t host_to_network(uint32_t input) { return htonl(input); } + return value; -template void append_to_buffer(vector *output, T value) { - value = host_to_network(value); - const uint8_t *bytes = reinterpret_cast(&value); - output->insert(output->end(), bytes, bytes + sizeof(T)); -} - -template -T get_from_buffer(const uint8_t **data, const uint8_t *end) { - const T *pointer = reinterpret_cast(*data); - *data = *data + sizeof(T); - if (*data <= end) { - return network_to_host(*pointer); - } else { - return 0; - } } void get_point_from_buffer(const uint8_t **data, const uint8_t *end, - Point *point) { + Point *point) { point->row = get_from_buffer(data, end); point->column = get_from_buffer(data, end); } diff --git a/src/core/patch.h b/src/core/patch.h index 78bd4c85..d887d166 100644 --- a/src/core/patch.h +++ b/src/core/patch.h @@ -1,3 +1,5 @@ +#pragma once + #include "optional.h" #include "point.h" #include "text.h" @@ -35,14 +37,14 @@ class Patch { Patch(Node *root, uint32_t hunk_count, bool merges_adjacent_hunks); Patch(Patch &&); ~Patch(); - bool splice(Point start, Point deletion_extent, Point insertion_extent, - std::unique_ptr old_text, std::unique_ptr new_text); + bool splice(Point start, Point deletion_extent, Point insertion_extent) { return this->splice(start, deletion_extent, insertion_extent, {}, {}); } + bool splice(Point start, Point deletion_extent, Point insertion_extent, std::unique_ptr old_text, std::unique_ptr new_text); bool splice_old(Point start, Point deletion_extent, Point insertion_extent); Patch copy(); Patch invert(); std::vector get_hunks() const; - std::vector get_hunks_in_new_range(Point start, Point end, - bool inclusive = false); + std::vector get_hunks_in_new_range(Point start, Point end) { return this->get_hunks_in_new_range(start, end, false); } + std::vector get_hunks_in_new_range(Point start, Point end, bool inclusive); std::vector get_hunks_in_old_range(Point start, Point end); optional hunk_for_old_position(Point position); optional hunk_for_new_position(Point position); diff --git a/src/core/text.h b/src/core/text.h index b9045ad3..8090b3ca 100644 --- a/src/core/text.h +++ b/src/core/text.h @@ -6,7 +6,12 @@ #include #include "point.h" -using Text = std::vector; +struct Text : public std::vector { + using std::vector::vector; + Text(void) : vector() {} + Text(std::vector const & vec) : vector(vec) {} + Text(std::vector && vec) : vector(std::move(vec)) {} +}; struct TextSlice { Text *text; diff --git a/test/js/marker-index.test.js b/test/js/marker-index.test.js index 7d4748f6..547182bb 100644 --- a/test/js/marker-index.test.js +++ b/test/js/marker-index.test.js @@ -10,7 +10,7 @@ describe('MarkerIndex', () => { let seed, seedMessage, random, markerIndex, markers, idCounter for (let i = 0; i < 1000; i++) { - seed = Date.now() + seed = 42;//Date.now() seedMessage = `Random Seed: ${seed}` random = new Random(seed) markerIndex = new MarkerIndex(seed) @@ -54,8 +54,8 @@ describe('MarkerIndex', () => { function verifyRanges () { for (let marker of markers) { let range = markerIndex.getRange(marker.id) - assert.deepEqual(range[0], marker.start, `Marker ${marker.id} start. ` + seedMessage) - assert.deepEqual(range[1], marker.end, `Marker ${marker.id} end. ` + seedMessage) + assert.deepEqual(range.start, marker.start, `Marker ${marker.id} start. ` + seedMessage) + assert.deepEqual(range.end, marker.end, `Marker ${marker.id} end. ` + seedMessage) } } @@ -304,7 +304,7 @@ describe('MarkerIndex', () => { let [{id}] = markers.splice(random(markers.length), 1) write(() => `delete ${id}`) assert(markerIndex.has(id), `Expected marker index to have ${id}. ` + seedMessage) - markerIndex.delete(id) + markerIndex.remove(id) assert(!markerIndex.has(id), `Expected marker index to not have ${id}. ` + seedMessage) } diff --git a/test/js/patch.test.js b/test/js/patch.test.js index d30c4b2c..ceecd9ef 100644 --- a/test/js/patch.test.js +++ b/test/js/patch.test.js @@ -11,7 +11,7 @@ const {Patch} = require('../..') describe('Patch', function () { it('honors the mergeAdjacentHunks option set to false', function () { - const patch = new Patch({mergeAdjacentHunks: false}) + const patch = new Patch({ mergeAdjacentHunks: false }) patch.splice({row: 0, column: 10}, {row: 0, column: 0}, {row: 1, column: 5}) patch.splice({row: 1, column: 5}, {row: 0, column: 2}, {row: 0, column: 8}) @@ -30,10 +30,12 @@ describe('Patch', function () { newEnd: {row: 1, column: 13} } ]) + + patch.delete(); }) it('honors the mergeAdjacentHunks option set to true', function () { - const patch = new Patch({mergeAdjacentHunks: true}) + const patch = new Patch({ mergeAdjacentHunks: true }) patch.splice({row: 0, column: 5}, {row: 0, column: 1}, {row: 0, column: 2}) patch.splice({row: 0, column: 10}, {row: 0, column: 3}, {row: 0, column: 4}) @@ -55,6 +57,8 @@ describe('Patch', function () { newStart: {row: 0, column: 5}, newEnd: {row: 0, column: 11} } ]) + + patch.delete(); }) it('can compose multiple patches together', function () { @@ -91,6 +95,11 @@ describe('Patch', function () { assert.throws(() => Patch.compose([{}, {}])) assert.throws(() => Patch.compose([1, 'a'])) + + for (let patch of patches) + patch.delete(); + + composedPatch.delete(); }) it('can invert patches', function () { @@ -127,6 +136,10 @@ describe('Patch', function () { newStart: {row: 0, column: 9}, newEnd: {row: 0, column: 14} } ]) + + patch.delete(); + invertedPatch.delete(); + patch2.delete(); }) it('can copy patches', function () { @@ -139,13 +152,16 @@ describe('Patch', function () { patch2.splice({row: 0, column: 3}, {row: 0, column: 4}, {row: 0, column: 5}) patch2.splice({row: 0, column: 10}, {row: 0, column: 5}, {row: 0, column: 5}) assert.deepEqual(patch2.copy().getHunks(), patch2.getHunks()) + + patch.delete(); + patch2.delete(); }) it('can serialize/deserialize patches', () => { const patch1 = new Patch() patch1.splice({row: 0, column: 3}, {row: 0, column: 5}, {row: 0, column: 5}, 'hello', 'world') - const patch2 = Patch.deserialize(Buffer.from(patch1.serialize().toString('base64'), 'base64')) + const patch2 = Patch.deserialize(patch1.serialize()) assert.deepEqual(JSON.parse(JSON.stringify(patch2.getHunks())), [{ oldStart: {row: 0, column: 3}, newStart: {row: 0, column: 3}, @@ -154,6 +170,9 @@ describe('Patch', function () { oldText: 'hello', newText: 'world' }]) + + patch1.delete(); + patch2.delete(); }) it('removes a hunk when it becomes empty', () => { @@ -181,7 +200,7 @@ describe('Patch', function () { const originalDocument = new TestDocument(seed) const mutatedDocument = originalDocument.clone() const mergeAdjacentHunks = random(2) - const patch = new Patch({mergeAdjacentHunks: mergeAdjacentHunks}) + const patch = new Patch({ mergeAdjacentHunks }) for (let j = 0; j < 20; j++) { if (random(10) < 1) { @@ -292,7 +311,7 @@ describe('Patch', function () { let oldPoint = originalDocument.buildRandomPoint() - let blob = Buffer.from(patch.serialize().toString('base64'), 'base64') + let blob = patch.serialize() const patchCopy1 = Patch.deserialize(blob) assert.deepEqual(patchCopy1.getHunks(), patch.getHunks(), seedMessage) assert.deepEqual(patchCopy1.hunkForOldPosition(oldPoint), patch.hunkForOldPosition(oldPoint), seedMessage) @@ -301,6 +320,8 @@ describe('Patch', function () { assert.deepEqual(patchCopy2.getHunks(), patch.getHunks(), seedMessage) assert.deepEqual(patchCopy2.hunkForOldPosition(oldPoint), patch.hunkForOldPosition(oldPoint), seedMessage) } + + patch.delete(); } }) })