From b908b9ac07f34660da83eea9a166970cb73e5fe1 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sun, 4 Feb 2024 21:18:41 -0300 Subject: [PATCH 01/79] Wip v2 --- package.json | 3 +- pnpm-lock.yaml | 278 +++++++++++++++++++++++++++++++++++-- src/frontend/typescript.ts | 8 +- src/v2/compilers.ts | 31 +++++ src/v2/index.ts | 4 + src/v2/iterator.ts | 64 +++++++++ src/v2/node.ts | 76 ++++++++++ src/v2/types.ts | 1 + 8 files changed, 452 insertions(+), 13 deletions(-) create mode 100644 src/v2/compilers.ts create mode 100644 src/v2/index.ts create mode 100644 src/v2/iterator.ts create mode 100644 src/v2/node.ts create mode 100644 src/v2/types.ts diff --git a/package.json b/package.json index 06d981d..f05e932 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,14 @@ "@fastify/cors": "^8.3.0", "fastify": "^4.19.2", "kleur": "^4.1.5", - "typescript": "5.1.6" + "typescript": "5.3.3" }, "devDependencies": { "@types/benchmark": "^2.1.2", "benchmark": "^2.1.4", "cli-table3": "^0.6.3", "pprof-it": "^1.2.1", + "tsx": "^4.7.0", "vite": "4.4.2", "vitest": "0.33.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eee53b2..5a84157 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ dependencies: specifier: ^4.1.5 version: 4.1.5 typescript: - specifier: 5.1.6 - version: 5.1.6 + specifier: 5.3.3 + version: 5.3.3 devDependencies: '@types/benchmark': @@ -31,6 +31,9 @@ devDependencies: pprof-it: specifier: ^1.2.1 version: 1.2.1 + tsx: + specifier: ^4.7.0 + version: 4.7.0 vite: specifier: 4.4.2 version: 4.4.2(@types/node@20.4.1) @@ -61,6 +64,15 @@ packages: split: 1.0.1 dev: true + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.18.11: resolution: {integrity: sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==} engines: {node: '>=12'} @@ -70,6 +82,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.18.11: resolution: {integrity: sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==} engines: {node: '>=12'} @@ -79,6 +100,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.18.11: resolution: {integrity: sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==} engines: {node: '>=12'} @@ -88,6 +118,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.18.11: resolution: {integrity: sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==} engines: {node: '>=12'} @@ -97,6 +136,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.18.11: resolution: {integrity: sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==} engines: {node: '>=12'} @@ -106,6 +154,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.18.11: resolution: {integrity: sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==} engines: {node: '>=12'} @@ -115,6 +172,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.18.11: resolution: {integrity: sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==} engines: {node: '>=12'} @@ -124,6 +190,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.18.11: resolution: {integrity: sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==} engines: {node: '>=12'} @@ -133,6 +208,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.18.11: resolution: {integrity: sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==} engines: {node: '>=12'} @@ -142,6 +226,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.18.11: resolution: {integrity: sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==} engines: {node: '>=12'} @@ -151,6 +244,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.18.11: resolution: {integrity: sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==} engines: {node: '>=12'} @@ -160,6 +262,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.18.11: resolution: {integrity: sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==} engines: {node: '>=12'} @@ -169,6 +280,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.18.11: resolution: {integrity: sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==} engines: {node: '>=12'} @@ -178,6 +298,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.18.11: resolution: {integrity: sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==} engines: {node: '>=12'} @@ -187,6 +316,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.18.11: resolution: {integrity: sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==} engines: {node: '>=12'} @@ -196,6 +334,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.18.11: resolution: {integrity: sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==} engines: {node: '>=12'} @@ -205,6 +352,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.18.11: resolution: {integrity: sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==} engines: {node: '>=12'} @@ -214,6 +370,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.18.11: resolution: {integrity: sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==} engines: {node: '>=12'} @@ -223,6 +388,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.18.11: resolution: {integrity: sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==} engines: {node: '>=12'} @@ -232,6 +406,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.18.11: resolution: {integrity: sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==} engines: {node: '>=12'} @@ -241,6 +424,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.18.11: resolution: {integrity: sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==} engines: {node: '>=12'} @@ -250,6 +442,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.18.11: resolution: {integrity: sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==} engines: {node: '>=12'} @@ -259,6 +460,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@fastify/ajv-compiler@3.5.0: resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} dependencies: @@ -559,6 +769,37 @@ packages: '@esbuild/win32-x64': 0.18.11 dev: true + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + dev: true + /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -662,8 +903,8 @@ packages: engines: {node: '>= 0.6'} dev: false - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true @@ -674,6 +915,12 @@ packages: resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} dev: true + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false @@ -939,6 +1186,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /ret@0.2.2: resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} engines: {node: '>=4'} @@ -958,7 +1209,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /safe-buffer@5.2.1: @@ -1104,13 +1355,24 @@ packages: engines: {node: '>=14.0.0'} dev: true + /tsx@4.7.0: + resolution: {integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.19.12 + get-tsconfig: 4.7.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} dev: true - /typescript@5.1.6: - resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true dev: false @@ -1180,7 +1442,7 @@ packages: postcss: 8.4.25 rollup: 3.26.2 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vitest@0.33.0: diff --git a/src/frontend/typescript.ts b/src/frontend/typescript.ts index 53260d0..0b71789 100644 --- a/src/frontend/typescript.ts +++ b/src/frontend/typescript.ts @@ -6,7 +6,7 @@ import { KindTable, ParsedProgram, Side } from "../shared/language"; import colorFn from "kleur"; import { fail } from "../debug"; -type TSNode = ts.Node & { text: string }; +type TsNode = ts.Node & { text: string }; export function getParsedProgram(side: Side, source: string): ParsedProgram { const sourceFile = getSourceFile(source); @@ -21,7 +21,7 @@ export function getParsedProgram(side: Side, source: string): ParsedProgram { } const nodes: Node[] = []; - function walk(node: TSNode) { + function walk(node: TsNode) { const isReservedWord = node.kind >= ts.SyntaxKind.FirstKeyword && node.kind <= ts.SyntaxKind.LastKeyword; const isPunctuation = node.kind >= ts.SyntaxKind.FirstPunctuation && node.kind <= ts.SyntaxKind.LastPunctuation; @@ -76,10 +76,10 @@ export function getParsedProgram(side: Side, source: string): ParsedProgram { nodes.push(newNode); } - node.getChildren().forEach((x) => walk(x as TSNode)); + node.getChildren().forEach((x) => walk(x as TsNode)); } - sourceFile.getChildren().forEach((x) => walk(x as TSNode)); + sourceFile.getChildren().forEach((x) => walk(x as TsNode)); const kindTable: KindTable = new Map(); diff --git a/src/v2/compilers.ts b/src/v2/compilers.ts new file mode 100644 index 0000000..12e468b --- /dev/null +++ b/src/v2/compilers.ts @@ -0,0 +1,31 @@ +import { Node as TsNode } from 'typescript'; +import { Node } from './node' +import { getSourceFile } from "../frontend/utils"; + +export function getAST(source: string) { + const ast = getSourceFile(source); + + let i = 0; + function walk(node: TsNode, parent: Node | undefined): Node { + const isLeafNode = node.getChildCount() === 0; + + const newNode = new Node({ + id: i, + kind: node.kind, + // In typescript a non-leaf node contains as string the whole children text combined, so we ignore it + text: isLeafNode ? node.getText() : '', + parent + }) + i++; + + if (!isLeafNode) { + newNode.children = node.getChildren().map(x => walk(x, newNode)); + } + + return newNode + } + + const newAst = walk(ast, undefined) + + return newAst +} \ No newline at end of file diff --git a/src/v2/index.ts b/src/v2/index.ts new file mode 100644 index 0000000..ebc901f --- /dev/null +++ b/src/v2/index.ts @@ -0,0 +1,4 @@ + +export function getDiff2(sourceA: string, sourceB: string): string { + return '' +} \ No newline at end of file diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts new file mode 100644 index 0000000..405ef5d --- /dev/null +++ b/src/v2/iterator.ts @@ -0,0 +1,64 @@ +import { Node } from "./node"; + +export class Iterator { + ast: Node + nodesQueue: Node[]; + lastNode!: Node; + + constructor(ast: Node) { + this.ast = ast; + this.nodesQueue = [ast]; + } + + next(startFrom?: Node): Node | undefined { + if (startFrom) { + return startFrom.next() + } + + // If it's the first time `next` is called we start from the root + if (!this.lastNode) { + return this.lastNode = this.ast + } + + // Otherwise we find the next in a breath-first order + const nextNode = this.lastNode.next() + + if (!nextNode) { + return undefined + } + + return this.lastNode = nextNode + } + + next2(startFrom?: Node): Node | undefined { + if (startFrom) { + return startFrom.next() + } + + if (this.nodesQueue.length === 0) { + return; + } + + const currentNode = this.nodesQueue.shift()!; + + if (!currentNode.isLeafNode()) { + this.nodesQueue.push(...currentNode.children); + } + + return currentNode + } + + nextBASE(): Node | undefined { + if (this.nodesQueue.length === 0) { + return; + } + + const currentNode = this.nodesQueue.shift()!; + + if (!currentNode.isLeafNode()) { + this.nodesQueue.push(...currentNode.children); + } + + return currentNode + } +} \ No newline at end of file diff --git a/src/v2/node.ts b/src/v2/node.ts new file mode 100644 index 0000000..ebca646 --- /dev/null +++ b/src/v2/node.ts @@ -0,0 +1,76 @@ +import { getPrettyKind } from "../debug"; +import { SyntaxKind } from "./types"; + +interface NewNodeArgs { + id: number; + kind: SyntaxKind; + text: string; + parent: Node | undefined; +} + +export class Node { + id: number; + kind: SyntaxKind; + text: string; + parent: Node | undefined; + children: Node[] = [] + + prettyKind: string; + + constructor(args: NewNodeArgs) { + const { id, kind, text, parent } = args + + this.id = id; + this.kind = kind; + this.text = text; + this.parent = parent + + this.prettyKind = getPrettyKind(kind) + + } + + isLeafNode() { + return this.children.length === 0; + } + + /* + Given the following AST: + + 1 + / \ + 2 5 + / \ + 3 4 + Steps: + - Start by return "2" + - Return "2" as it's the first children + - "2" is not a leaf node, return the first child, "3" + - "3" is a leaf node, go back to "2" passing it's id. We find that there is a sibling, return "4" + - "4" is a leaf node, go back to "2" passing it's id. No more siblings are found, call next in the parent "1", there the next sibling is "5" + - No more node, ended iteration + */ + + next(startAfterNodeId?: number): Node | undefined { + // Case 1: Go back to the parent + if (this.isLeafNode()) { + return this.parent!.next(this.id) + } + + // Case 2: We come from the parent + if (startAfterNodeId) { + const previousIndex = this.children.findIndex(a => a.id === startAfterNodeId) + const hasNext = this.children[previousIndex + 1] + + if (hasNext) { + // Case 2a: There was a sibling + return hasNext + } else { + // Case 2b: There was no sibling + return this.parent?.next(this.id) + } + } + + // Case 3: Return first children + return this.children[0] + } +} \ No newline at end of file diff --git a/src/v2/types.ts b/src/v2/types.ts new file mode 100644 index 0000000..e1e7752 --- /dev/null +++ b/src/v2/types.ts @@ -0,0 +1 @@ +export type SyntaxKind = number \ No newline at end of file From ab87add699ed136ebb04b57e602f25855c55c23b Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sun, 4 Feb 2024 22:02:38 -0300 Subject: [PATCH 02/79] Wippp --- src/debug.ts | 2 +- src/server.ts | 12 +++--- src/types.ts | 29 +++++++++++++ src/v2/iterator.ts | 32 -------------- v2.md | 105 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 39 deletions(-) create mode 100644 v2.md diff --git a/src/debug.ts b/src/debug.ts index 2658954..900d05c 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -10,7 +10,7 @@ enum ErrorType { } class BaseError { - constructor(public type: ErrorType, public message: string, public serializedError?: string, public extra?: unknown) {} + constructor(public type: ErrorType, public message: string, public serializedError?: string, public extra?: unknown) { } } class DebugFailure extends BaseError { diff --git a/src/server.ts b/src/server.ts index 7764eec..08a5a71 100644 --- a/src/server.ts +++ b/src/server.ts @@ -12,15 +12,15 @@ const server = fastify({ logger: true }); server.post("/", ({ body }, _reply) => { const { a, b } = JSON.parse(body as string) as GetDiffPayload; - server.log.info({ - message: "About to process", - a, - b, - }); + // server.log.info({ + // message: "About to process", + // a, + // b, + // }); console.time("diff took"); - const res = getDiff(a, b, { outputType: OutputType.serializedAlignedChunks, alignmentText: ("<>") }); + const res = getDiff(a, b, { outputType: OutputType.serializedChunks, alignmentText: ("<>") }); console.timeEnd("diff took"); return res; diff --git a/src/types.ts b/src/types.ts index 28fe48f..cb46c2b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ + + export enum DiffType { deletion = 1 << 0, // 1 addition = 1 << 1, // 2 @@ -10,11 +12,38 @@ export const TypeMasks = { AddOrMove: DiffType.addition | DiffType.move, }; +export interface NewDiffInfo { + index: number; + range: Range; +} + export interface Range { start: number; end: number; } +export enum RenderInstruction { + // No text decoration + default = "default", + + // Text with color + addition = "addition", + deletion = "deletion", + move = "move", +} + +export interface SourceChunk { + type: RenderInstruction; + text: string; + moveNumber: string; +} + +// What the frontend sends +export interface GetDiffPayload { + a: string; + b: string; +} + export enum Mode { release, debug, diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index 405ef5d..0ed1207 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -29,36 +29,4 @@ export class Iterator { return this.lastNode = nextNode } - - next2(startFrom?: Node): Node | undefined { - if (startFrom) { - return startFrom.next() - } - - if (this.nodesQueue.length === 0) { - return; - } - - const currentNode = this.nodesQueue.shift()!; - - if (!currentNode.isLeafNode()) { - this.nodesQueue.push(...currentNode.children); - } - - return currentNode - } - - nextBASE(): Node | undefined { - if (this.nodesQueue.length === 0) { - return; - } - - const currentNode = this.nodesQueue.shift()!; - - if (!currentNode.isLeafNode()) { - this.nodesQueue.push(...currentNode.children); - } - - return currentNode - } } \ No newline at end of file diff --git a/v2.md b/v2.md new file mode 100644 index 0000000..acb48a6 --- /dev/null +++ b/v2.md @@ -0,0 +1,105 @@ +This is a high-level overview of the second version of the algorithm. I'm reworking the whole system to solve the following issues that arise during the development of the previous version and, despite the efforts, I couldn't build the existing architecture to solve them. So I'm starting from scratch with those limitations in mind, these are: + +# High prio + +## Score based matching +Before we would only match perfect sequences, this means that the slightest of changes would disrupt the matching. +In the new version, we are going to have a score function that would assign a score from (tentatively) 0 to 100 and the highest scoring subsequence will be picked. + +Some notes: +- Still need to define the score function, it could include the following data + - Text content + - Kind + - Line and position (close changes are preferred) + - Number of nodes in between (fewer nodes in between preferred) +- Score should be above a given threshold to match + +Example +```ts +function asd() { + return 123 +} + +// vs + +function ddd() { + return 333 +} +``` + +This should match a single move + +## Moves now contain sections +Before we would create a move saying, "From X to Y it moved to A and B, limiting us to a single section, this is why we had to include the closing node as a separate field. Also, this single-section approach will limit us considering the above point where now a move could be, for example: +"perfect match", "addition", "perfect match" + +So moves should have an array of sections + +TODO: Check if we can remove the open/close verifier with this + +## Use all the AST nodesData changes +Right now we are using the "textual" nodes, but we should use the full AST. For example, to solve the semantic diff case that arises with JS ASI +```js +function asd() { + return 1 +} + +// vs + +function asd() { + return + 1 +} +``` + +This will also simplify the language setup as you won't need to include which nodes are considered "textual". TODO: Or at least not in TS because we can check if a node is the leaf node. Unsure if this assumption will hold for all other compilers + +## Alignment +Haven't planned how to implement this but this has to be for version 1.0 + +## Algo steps visualizer +We should create a `recordStep` function that tracks the current "Thinking" for the algorithm. This should be visualized in the front end (or terminal) by clicking forward / backwards. +This is going to be pretty important to debug the algo, especially useful in tricky scenarios + +# Low prio + +## Better organize code +We should have the possibility to override functions to experiment with new behaviours, for example how we diff code either with the string-to-string method or the zigzag algo + +## UNSURE | Use AST instead of flattened array +TODO: Think why this is needed. In any case, we can have the iterator abstract this away + +## Intra-node diffing +In the current approach, we consider the following a change + +```ts +123123 +'asdasd' + +// --- +1233123 +'asddasd' +``` + +While is true a better output would be to use Myers algo to diff the values + +## Change type +I tinkered with this idea in the past, some experimentation is needed, the idea is that in some cases, instead of reporting an addition and deletion we can link both in a "change" for example: + +```js +const value = 123 + +// vs + +const value = true +``` + +## Cross file diffs + +## Brainstorm 3-way merge + +## Highlighting +Maybe we have some business here + +## Tree sitter & other languages +Also having languages communicate via RPC or something, right now we are loading typescript in a JS module, this will prevent us from using a non-js compiler From abf14e75dff14566c2458510aacc53ddc8591235 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 5 Feb 2024 17:34:34 -0300 Subject: [PATCH 03/79] Workingggg --- src/backend/printer.ts | 6 ++++++ src/v2/compilers.ts | 36 +++++++++++++++++++++++++++++++++-- src/v2/diff.ts | 22 +++++++++++++++++++++ src/v2/index.ts | 40 ++++++++++++++++++++++++++++++++++++--- src/v2/iterator.ts | 43 +++++++++++++++++++++++++++++++++++++++++- src/v2/node.ts | 30 +++++++++++++++++++++++++++-- src/v2/types.ts | 11 ++++++++++- src/v2/utils.ts | 12 ++++++++++++ v2.md | 3 +++ 9 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 src/v2/diff.ts create mode 100644 src/v2/utils.ts diff --git a/src/backend/printer.ts b/src/backend/printer.ts index ba4452e..94c0430 100644 --- a/src/backend/printer.ts +++ b/src/backend/printer.ts @@ -77,6 +77,12 @@ export function getSourceWithChange( end: number, colorFn: RenderFn, ) { + // This is to handle cases like EndOfFile + // TODO: Think if this is the best way to handle this, maybe we can just ignore the EOF node altogether or modify it + if (start === end) { + return chars + } + const head = chars.slice(0, start); const text = chars.slice(start, end).join(""); diff --git a/src/v2/compilers.ts b/src/v2/compilers.ts index 12e468b..c0e019b 100644 --- a/src/v2/compilers.ts +++ b/src/v2/compilers.ts @@ -1,23 +1,42 @@ import { Node as TsNode } from 'typescript'; import { Node } from './node' import { getSourceFile } from "../frontend/utils"; +import { Side } from '../shared/language'; +import { NodesTable, ParsedProgram } from './types'; -export function getAST(source: string) { +export function getAST(source: string, side: Side): ParsedProgram { const ast = getSourceFile(source); + const nodesTable: NodesTable = new Map() let i = 0; function walk(node: TsNode, parent: Node | undefined): Node { const isLeafNode = node.getChildCount() === 0; + // Each node owns the trivia before until the previous token, for example: + // + // age = 24 + // ^ + // Trivia for the number literal starts here, but you don't want to start the diff here + // + // This is why we add the leading trivia to the `start` of the node, so we get where the actual + // value of the node starts and not where the trivia starts + const start = node.pos + node.getLeadingTriviaWidth(); + const end = node.end + const newNode = new Node({ + side, id: i, kind: node.kind, // In typescript a non-leaf node contains as string the whole children text combined, so we ignore it text: isLeafNode ? node.getText() : '', + start, + end, parent }) i++; + storeNodeInNodeTable(nodesTable, newNode) + if (!isLeafNode) { newNode.children = node.getChildren().map(x => walk(x, newNode)); } @@ -27,5 +46,18 @@ export function getAST(source: string) { const newAst = walk(ast, undefined) - return newAst + return { + ast: newAst, + nodesTable + } +} + +function storeNodeInNodeTable(nodesTable: NodesTable, node: Node) { + const currentValue = nodesTable.get(node.kind); + + if (currentValue) { + currentValue.push(node); + } else { + nodesTable.set(node.kind, [node]); + } } \ No newline at end of file diff --git a/src/v2/diff.ts b/src/v2/diff.ts new file mode 100644 index 0000000..3fe0e44 --- /dev/null +++ b/src/v2/diff.ts @@ -0,0 +1,22 @@ +import { DiffType } from "../types"; +import { Node } from './node' + +interface ChangeSegment { + type: DiffType; + start: Node + end: Node +} + +export class Change { + type: DiffType + segments: ChangeSegment[] + + constructor(type: DiffType, segments: ChangeSegment[]) { + this.type = type + this.segments = segments + } +} + +export function findBestSequence(a: Node, b: Node) { + +} \ No newline at end of file diff --git a/src/v2/index.ts b/src/v2/index.ts index ebc901f..bfa634e 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,4 +1,38 @@ +import { Context } from "./utils"; +import { Side } from "../shared/language"; +import { Iterator } from "./iterator"; +import { Change, findBestSequence } from "./diff"; -export function getDiff2(sourceA: string, sourceB: string): string { - return '' -} \ No newline at end of file +export function getDiff2(sourceA: string, sourceB: string) { + const changes: Change[] = []; + + const iterA = new Iterator(sourceA, Side.a) + const iterB = new Iterator(sourceB, Side.b) + + _context = new Context(sourceA, sourceB); + + _context.iterA = iterA; + _context.iterB = iterB; + + let i = 0 + while (true) { + const a = iterA.next() + const b = iterB.next() + + // No more nodes to process. We are done + if (!a && !b) { + break + } + + // One of the iterators ran out of nodes. We will mark the remaining unmatched nodes in the other iterator + // as deletions or additions and exit. + if (!a || !b) { + break + } + + findBestSequence(a, b) + + } +} + +export let _context: Context; \ No newline at end of file diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index 0ed1207..38d7d88 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -1,16 +1,44 @@ +import { _context } from "."; +import { getSourceWithChange } from "../backend/printer"; +import { Side } from "../shared/language"; +import { getAST } from "./compilers"; import { Node } from "./node"; +import colorFn from "kleur"; +import { NodesTable } from "./types"; export class Iterator { ast: Node nodesQueue: Node[]; lastNode!: Node; + source: string + side: Side + + nodesTable: NodesTable + + constructor(source: string, side: Side) { + this.side = side + this.source = source + const { ast, nodesTable } = getAST(source, side) - constructor(ast: Node) { this.ast = ast; + this.nodesTable = nodesTable + this.nodesQueue = [ast]; } next(startFrom?: Node): Node | undefined { + const nextNode = this._next(startFrom) + + // TODO: The first node could be matched already + if (!nextNode) { + this.lastNode = this.ast + return this.ast + } + + return nextNode + } + + _next(startFrom?: Node): Node | undefined { if (startFrom) { return startFrom.next() } @@ -29,4 +57,17 @@ export class Iterator { return this.lastNode = nextNode } + + printNode(node: Node) { + const source = this.side === Side.a ? _context.sourceA : _context.sourceB; + const chars = source.split(""); + + const { start, end } = node.getRange(); + + const color = node.isLeafNode() ? colorFn.magenta : colorFn.yellow + const result = getSourceWithChange(chars, start, end, color); + + console.log(result.join("")); + } + } \ No newline at end of file diff --git a/src/v2/node.ts b/src/v2/node.ts index ebca646..7e0adcf 100644 --- a/src/v2/node.ts +++ b/src/v2/node.ts @@ -1,28 +1,41 @@ import { getPrettyKind } from "../debug"; import { SyntaxKind } from "./types"; - +import { Range } from '../types' +import { Side } from "../shared/language"; +import { _context } from "."; interface NewNodeArgs { + side: Side id: number; kind: SyntaxKind; text: string; + + start: number; + end: number + parent: Node | undefined; } export class Node { + side: Side id: number; kind: SyntaxKind; text: string; + start: number; + end: number parent: Node | undefined; children: Node[] = [] prettyKind: string; constructor(args: NewNodeArgs) { - const { id, kind, text, parent } = args + const { side, id, kind, text, parent, start, end } = args + this.side = side this.id = id; this.kind = kind; this.text = text; + this.start = start + this.end = end this.parent = parent this.prettyKind = getPrettyKind(kind) @@ -33,6 +46,13 @@ export class Node { return this.children.length === 0; } + getRange(): Range { + return { + start: this.start, + end: this.end, + }; + } + /* Given the following AST: @@ -73,4 +93,10 @@ export class Node { // Case 3: Return first children return this.children[0] } + + draw() { + const iter = _context[this.side === Side.a ? "iterA" : "iterB"]; + + iter.printNode(this); + } } \ No newline at end of file diff --git a/src/v2/types.ts b/src/v2/types.ts index e1e7752..ed90008 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -1 +1,10 @@ -export type SyntaxKind = number \ No newline at end of file +import { Node } from "./node" + +export type SyntaxKind = number + +export type NodesTable = Map + +export interface ParsedProgram { + ast: Node; + nodesTable: NodesTable; +} \ No newline at end of file diff --git a/src/v2/utils.ts b/src/v2/utils.ts new file mode 100644 index 0000000..6c781d3 --- /dev/null +++ b/src/v2/utils.ts @@ -0,0 +1,12 @@ +import { Iterator } from "./iterator"; + +export class Context { + // Iterators will get stored once they are initialize, which happens later on the execution + iterA!: Iterator; + iterB!: Iterator; + + constructor( + public sourceA: string, + public sourceB: string, + ) { } +} diff --git a/v2.md b/v2.md index acb48a6..a40dc0b 100644 --- a/v2.md +++ b/v2.md @@ -57,6 +57,8 @@ This will also simplify the language setup as you won't need to include which no ## Alignment Haven't planned how to implement this but this has to be for version 1.0 +Also we need to consider the case where `startLine !== endLine` form example tagged template literals + ## Algo steps visualizer We should create a `recordStep` function that tracks the current "Thinking" for the algorithm. This should be visualized in the front end (or terminal) by clicking forward / backwards. This is going to be pretty important to debug the algo, especially useful in tricky scenarios @@ -103,3 +105,4 @@ Maybe we have some business here ## Tree sitter & other languages Also having languages communicate via RPC or something, right now we are loading typescript in a JS module, this will prevent us from using a non-js compiler +Maybe here we can also conside sublanguages, an HTML file can have JS and CSS as well as HTML code \ No newline at end of file From b99177ccd782d3c48d4d84161d8e34b28d3165ba Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 5 Feb 2024 18:38:20 -0300 Subject: [PATCH 04/79] Some tests --- src/v2/node.ts | 5 ++ tests/v2/tree-walker.test.ts | 124 +++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 tests/v2/tree-walker.test.ts diff --git a/src/v2/node.ts b/src/v2/node.ts index 7e0adcf..a970222 100644 --- a/src/v2/node.ts +++ b/src/v2/node.ts @@ -25,6 +25,7 @@ export class Node { parent: Node | undefined; children: Node[] = [] + matched = false prettyKind: string; constructor(args: NewNodeArgs) { @@ -53,6 +54,10 @@ export class Node { }; } + mark() { + this.matched = true; + } + /* Given the following AST: diff --git a/tests/v2/tree-walker.test.ts b/tests/v2/tree-walker.test.ts new file mode 100644 index 0000000..c74ddee --- /dev/null +++ b/tests/v2/tree-walker.test.ts @@ -0,0 +1,124 @@ +import { expect, test } from "vitest"; +import { Iterator } from "../../src/v2/iterator"; +import { Side } from "../../src/shared/language"; + +const source = "1 + (2 + 3)" +const expectedNodes = [ + "SourceFile", + "SyntaxList", + "ExpressionStatement", + "BinaryExpression", + "NumericLiteral", + "PlusToken", + "ParenthesizedExpression", + "OpenParenToken", + "BinaryExpression", + "NumericLiteral", + "PlusToken", + "NumericLiteral", + "CloseParenToken", + "EndOfFileToken", +]; + +test("Ensure the tree walker visits all nodes in order", () => { + const iter = new Iterator(source, Side.a) + + // Prime the walker so that we skip the "SourceFile", we will use that to detect when the iteration start over + const nodes: string[] = [iter.next()!.prettyKind] + + while (true) { + const next = iter.next()!.prettyKind + + if (next === 'SourceFile') { + break + } + + nodes.push(next) + } + + expect(nodes).toMatchObject(expectedNodes); +}); + +test("Ensure the tree walker visits all nodes by passing one node that the time", () => { + const iter = new Iterator(source, Side.a) + + const nodes: string[] = [] + + const one = iter.next(); + nodes.push(one!.prettyKind) + const two = iter.next(one); + nodes.push(two!.prettyKind) + const three = iter.next(two); + nodes.push(three!.prettyKind) + const four = iter.next(three); + nodes.push(four!.prettyKind) + const five = iter.next(four); + nodes.push(five!.prettyKind) + const six = iter.next(five); + nodes.push(six!.prettyKind) + const seven = iter.next(six); + nodes.push(seven!.prettyKind) + const eight = iter.next(seven); + nodes.push(eight!.prettyKind) + const nine = iter.next(eight); + nodes.push(nine!.prettyKind) + const ten = iter.next(nine); + nodes.push(ten!.prettyKind) + const eleven = iter.next(ten); + nodes.push(eleven!.prettyKind) + const twelve = iter.next(eleven); + nodes.push(twelve!.prettyKind) + const thirteen = iter.next(twelve); + nodes.push(thirteen!.prettyKind) + const fourteen = iter.next(thirteen); + nodes.push(fourteen!.prettyKind) + + expect(nodes).toMatchObject(expectedNodes); +}); + +test("Ensure the tree walker loops back after visiting all the nodes", () => { + const iter = new Iterator(source, Side.a) + + let lastNode; + + let i = 0; + while (i < expectedNodes.length) { + lastNode = iter.next() + i++ + } + + const shouldBeFirstNode = iter.next()!.prettyKind + + expect(shouldBeFirstNode).toBe(expectedNodes[0]) +}); + +test("Ensure the tree walker ignores matched nodes", () => { + const iter = new Iterator(source, Side.a) + + const nodesToMatch = [0, 2, 5, 11] + + // First pass over the node to mark a few as matched + while (true) { + const node = iter.next()! + + if (node.prettyKind === "EndOfFileToken") break + + if (nodesToMatch.includes(node.id)) { + console.log(node.prettyKind) + node.mark() + } + } + + const nodes = [] + // Second pass to gather the non-matched nodes + while (true) { + const node = iter.next()! + + if (node.prettyKind === "EndOfFileToken") break + + nodes.push(node) + } + + expect(nodes.length).toBe(10) + +}); \ No newline at end of file From eba240cf5667c32d23b5f6f4e3c86edadf99f4ea Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 5 Feb 2024 18:38:29 -0300 Subject: [PATCH 05/79] test command --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f05e932..6659624 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "scripts": { "test": "vitest -c ./vitest.config.js", + "test-v2": "vitest -c ./vitest.config.js ./tests/v2", "build": "tsc --watch", "check": "npm run format && npm run lint", "format": "deno fmt", @@ -36,4 +37,4 @@ "vite": "4.4.2", "vitest": "0.33.0" } -} +} \ No newline at end of file From d3a717982cdcf8866cdbaa369b1f4bd616ce8858 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 5 Feb 2024 19:30:14 -0300 Subject: [PATCH 06/79] Updates --- src/v2/iterator.ts | 83 ++++++++++++++++++++++++++++-------- tests/v2/tree-walker.test.ts | 30 ++++++++++--- 2 files changed, 89 insertions(+), 24 deletions(-) diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index 38d7d88..9202b96 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -26,38 +26,85 @@ export class Iterator { this.nodesQueue = [ast]; } - next(startFrom?: Node): Node | undefined { - const nextNode = this._next(startFrom) + next(_startFrom?: Node): Node | undefined { + const nextNode = this._next(_startFrom) - // TODO: The first node could be matched already if (!nextNode) { - this.lastNode = this.ast - return this.ast + return this.lastNode = this.ast } - return nextNode + this.lastNode = nextNode; + return nextNode; } - _next(startFrom?: Node): Node | undefined { - if (startFrom) { - return startFrom.next() + _next(_startFrom?: Node): Node | undefined { + let current = _startFrom || this.lastNode + + // Special case if we are in the first iteration, return the root node + if (!current) { + return this.ast } - // If it's the first time `next` is called we start from the root - if (!this.lastNode) { - return this.lastNode = this.ast + // Node has children, lets go to the first one + if (!current.isLeafNode()) { + return current.children[0] } - // Otherwise we find the next in a breath-first order - const nextNode = this.lastNode.next() + // If we are in a leaf node we need to + // - Go to the sibling node, if exist, or + // - Go up and find a sibling node there + while (true) { + const parent = current.parent - if (!nextNode) { - return undefined - } - return this.lastNode = nextNode + if (!parent) { + return + } + + // We found a node with children, we need to check if there are any sibling remaining + const currentNodeIndex = parent.children.findIndex(x => x.id === current.id) + const hasSibling = parent.children[currentNodeIndex + 1] + + if (hasSibling) { + return hasSibling + } + + current = current.parent! + } } + // next2(startFrom?: Node): Node | undefined { + // const nextNode = this._next2(startFrom) + + // // TODO: The first node could be matched already + // if (!nextNode) { + // this.lastNode = this.ast + // return this.ast + // } + + // return nextNode + // } + + // _next2(startFrom?: Node): Node | undefined { + // if (startFrom) { + // return startFrom.next() + // } + + // // If it's the first time `next` is called we start from the root + // if (!this.lastNode) { + // return this.lastNode = this.ast + // } + + // // Otherwise we find the next in a breath-first order + // const nextNode = this.lastNode.next() + + // if (!nextNode) { + // return undefined + // } + + // return this.lastNode = nextNode + // } + printNode(node: Node) { const source = this.side === Side.a ? _context.sourceA : _context.sourceB; const chars = source.split(""); diff --git a/tests/v2/tree-walker.test.ts b/tests/v2/tree-walker.test.ts index c74ddee..5667e24 100644 --- a/tests/v2/tree-walker.test.ts +++ b/tests/v2/tree-walker.test.ts @@ -95,18 +95,36 @@ test("Ensure the tree walker loops back after visiting all the nodes", () => { test("Ensure the tree walker ignores matched nodes", () => { const iter = new Iterator(source, Side.a) - const nodesToMatch = [0, 2, 5, 11] + // Given the full node list, lets ignore the following + const expectedNodes2 = [ + //"SourceFile", // index 0, root node + "SyntaxList", + "ExpressionStatement", + "BinaryExpression", + "NumericLiteral", + "PlusToken", + "ParenthesizedExpression", + //"OpenParenToken", // index 7, has siblings + "BinaryExpression", + "NumericLiteral", + "PlusToken", + //"NumericLiteral", // index 11, last sibling + "CloseParenToken", + "EndOfFileToken", + ]; + + const nodesToMatch = [0, 7, 11] // First pass over the node to mark a few as matched while (true) { const node = iter.next()! - if (node.prettyKind === "EndOfFileToken") break - if (nodesToMatch.includes(node.id)) { console.log(node.prettyKind) node.mark() } + + if (node.prettyKind === "EndOfFileToken") break } const nodes = [] @@ -114,11 +132,11 @@ test("Ensure the tree walker ignores matched nodes", () => { while (true) { const node = iter.next()! - if (node.prettyKind === "EndOfFileToken") break - nodes.push(node) + + if (node.prettyKind === "EndOfFileToken") break } - expect(nodes.length).toBe(10) + expect(nodes.length).toBe(expectedNodes2.length) }); \ No newline at end of file From 7790e04edac0da6d08ce3ed3a6b4849322c5941b Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 5 Feb 2024 19:49:53 -0300 Subject: [PATCH 07/79] Lets roll --- src/v2/iterator.ts | 57 +++++++++++++----------------------- src/v2/node.ts | 41 -------------------------- tests/v2/tree-walker.test.ts | 7 +++-- 3 files changed, 24 insertions(+), 81 deletions(-) diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index 9202b96..a5eb1d9 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -27,14 +27,25 @@ export class Iterator { } next(_startFrom?: Node): Node | undefined { - const nextNode = this._next(_startFrom) + let nextNode + while (true) { + nextNode = this._next(_startFrom) + + if (!nextNode) { + if (this.ast.matched) { + return undefined + } else { + return this.lastNode = this.ast + } + } - if (!nextNode) { - return this.lastNode = this.ast + if (nextNode.matched) { + return this.next(nextNode) + } else { + this.lastNode = nextNode; + return nextNode + } } - - this.lastNode = nextNode; - return nextNode; } _next(_startFrom?: Node): Node | undefined { @@ -73,37 +84,9 @@ export class Iterator { } } - // next2(startFrom?: Node): Node | undefined { - // const nextNode = this._next2(startFrom) - - // // TODO: The first node could be matched already - // if (!nextNode) { - // this.lastNode = this.ast - // return this.ast - // } - - // return nextNode - // } - - // _next2(startFrom?: Node): Node | undefined { - // if (startFrom) { - // return startFrom.next() - // } - - // // If it's the first time `next` is called we start from the root - // if (!this.lastNode) { - // return this.lastNode = this.ast - // } - - // // Otherwise we find the next in a breath-first order - // const nextNode = this.lastNode.next() - - // if (!nextNode) { - // return undefined - // } - - // return this.lastNode = nextNode - // } + resetWalkingOrder() { + this.lastNode = this.ast + } printNode(node: Node) { const source = this.side === Side.a ? _context.sourceA : _context.sourceB; diff --git a/src/v2/node.ts b/src/v2/node.ts index a970222..93402b5 100644 --- a/src/v2/node.ts +++ b/src/v2/node.ts @@ -58,47 +58,6 @@ export class Node { this.matched = true; } - /* - Given the following AST: - - 1 - / \ - 2 5 - / \ - 3 4 - Steps: - - Start by return "2" - - Return "2" as it's the first children - - "2" is not a leaf node, return the first child, "3" - - "3" is a leaf node, go back to "2" passing it's id. We find that there is a sibling, return "4" - - "4" is a leaf node, go back to "2" passing it's id. No more siblings are found, call next in the parent "1", there the next sibling is "5" - - No more node, ended iteration - */ - - next(startAfterNodeId?: number): Node | undefined { - // Case 1: Go back to the parent - if (this.isLeafNode()) { - return this.parent!.next(this.id) - } - - // Case 2: We come from the parent - if (startAfterNodeId) { - const previousIndex = this.children.findIndex(a => a.id === startAfterNodeId) - const hasNext = this.children[previousIndex + 1] - - if (hasNext) { - // Case 2a: There was a sibling - return hasNext - } else { - // Case 2b: There was no sibling - return this.parent?.next(this.id) - } - } - - // Case 3: Return first children - return this.children[0] - } - draw() { const iter = _context[this.side === Side.a ? "iterA" : "iterB"]; diff --git a/tests/v2/tree-walker.test.ts b/tests/v2/tree-walker.test.ts index 5667e24..acdabcc 100644 --- a/tests/v2/tree-walker.test.ts +++ b/tests/v2/tree-walker.test.ts @@ -120,23 +120,24 @@ test("Ensure the tree walker ignores matched nodes", () => { const node = iter.next()! if (nodesToMatch.includes(node.id)) { - console.log(node.prettyKind) node.mark() } if (node.prettyKind === "EndOfFileToken") break } + iter.resetWalkingOrder() + const nodes = [] // Second pass to gather the non-matched nodes while (true) { const node = iter.next()! - nodes.push(node) + nodes.push(node.prettyKind) if (node.prettyKind === "EndOfFileToken") break } - expect(nodes.length).toBe(expectedNodes2.length) + expect(nodes).toMatchObject(expectedNodes2) }); \ No newline at end of file From 4c9cbac46679834ea76153b670a229a0b1ed40d2 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 5 Feb 2024 20:12:57 -0300 Subject: [PATCH 08/79] fmt --- src/backend/printer.ts | 2 +- src/debug.ts | 2 +- src/types.ts | 2 - src/v2/compilers.ts | 32 ++++++------ src/v2/diff.ts | 18 +++---- src/v2/index.ts | 19 ++++--- src/v2/iterator.ts | 54 ++++++++++---------- src/v2/node.ts | 29 ++++++----- src/v2/types.ts | 8 +-- src/v2/utils.ts | 2 +- tests/v2/tree-walker.test.ts | 96 +++++++++++++++++++++--------------- 11 files changed, 136 insertions(+), 128 deletions(-) diff --git a/src/backend/printer.ts b/src/backend/printer.ts index 94c0430..c79fd15 100644 --- a/src/backend/printer.ts +++ b/src/backend/printer.ts @@ -80,7 +80,7 @@ export function getSourceWithChange( // This is to handle cases like EndOfFile // TODO: Think if this is the best way to handle this, maybe we can just ignore the EOF node altogether or modify it if (start === end) { - return chars + return chars; } const head = chars.slice(0, start); diff --git a/src/debug.ts b/src/debug.ts index 900d05c..2658954 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -10,7 +10,7 @@ enum ErrorType { } class BaseError { - constructor(public type: ErrorType, public message: string, public serializedError?: string, public extra?: unknown) { } + constructor(public type: ErrorType, public message: string, public serializedError?: string, public extra?: unknown) {} } class DebugFailure extends BaseError { diff --git a/src/types.ts b/src/types.ts index cb46c2b..53540e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,3 @@ - - export enum DiffType { deletion = 1 << 0, // 1 addition = 1 << 1, // 2 diff --git a/src/v2/compilers.ts b/src/v2/compilers.ts index c0e019b..891fd94 100644 --- a/src/v2/compilers.ts +++ b/src/v2/compilers.ts @@ -1,12 +1,12 @@ -import { Node as TsNode } from 'typescript'; -import { Node } from './node' +import { Node as TsNode } from "typescript"; +import { Node } from "./node"; import { getSourceFile } from "../frontend/utils"; -import { Side } from '../shared/language'; -import { NodesTable, ParsedProgram } from './types'; +import { Side } from "../shared/language"; +import { NodesTable, ParsedProgram } from "./types"; export function getAST(source: string, side: Side): ParsedProgram { const ast = getSourceFile(source); - const nodesTable: NodesTable = new Map() + const nodesTable: NodesTable = new Map(); let i = 0; function walk(node: TsNode, parent: Node | undefined): Node { @@ -21,35 +21,35 @@ export function getAST(source: string, side: Side): ParsedProgram { // This is why we add the leading trivia to the `start` of the node, so we get where the actual // value of the node starts and not where the trivia starts const start = node.pos + node.getLeadingTriviaWidth(); - const end = node.end + const end = node.end; const newNode = new Node({ side, id: i, kind: node.kind, // In typescript a non-leaf node contains as string the whole children text combined, so we ignore it - text: isLeafNode ? node.getText() : '', + text: isLeafNode ? node.getText() : "", start, end, - parent - }) + parent, + }); i++; - storeNodeInNodeTable(nodesTable, newNode) + storeNodeInNodeTable(nodesTable, newNode); if (!isLeafNode) { - newNode.children = node.getChildren().map(x => walk(x, newNode)); + newNode.children = node.getChildren().map((x) => walk(x, newNode)); } - return newNode + return newNode; } - const newAst = walk(ast, undefined) + const newAst = walk(ast, undefined); return { ast: newAst, - nodesTable - } + nodesTable, + }; } function storeNodeInNodeTable(nodesTable: NodesTable, node: Node) { @@ -60,4 +60,4 @@ function storeNodeInNodeTable(nodesTable: NodesTable, node: Node) { } else { nodesTable.set(node.kind, [node]); } -} \ No newline at end of file +} diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 3fe0e44..4353029 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -1,22 +1,20 @@ import { DiffType } from "../types"; -import { Node } from './node' +import { Node } from "./node"; interface ChangeSegment { type: DiffType; - start: Node - end: Node + start: Node; + end: Node; } export class Change { - type: DiffType - segments: ChangeSegment[] + type: DiffType; + segments: ChangeSegment[]; constructor(type: DiffType, segments: ChangeSegment[]) { - this.type = type - this.segments = segments + this.type = type; + this.segments = segments; } } -export function findBestSequence(a: Node, b: Node) { - -} \ No newline at end of file +export function findBestSequence(a: Node, b: Node) {} diff --git a/src/v2/index.ts b/src/v2/index.ts index bfa634e..dff36f9 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -6,33 +6,32 @@ import { Change, findBestSequence } from "./diff"; export function getDiff2(sourceA: string, sourceB: string) { const changes: Change[] = []; - const iterA = new Iterator(sourceA, Side.a) - const iterB = new Iterator(sourceB, Side.b) + const iterA = new Iterator(sourceA, Side.a); + const iterB = new Iterator(sourceB, Side.b); _context = new Context(sourceA, sourceB); _context.iterA = iterA; _context.iterB = iterB; - let i = 0 + let i = 0; while (true) { - const a = iterA.next() - const b = iterB.next() + const a = iterA.next(); + const b = iterB.next(); // No more nodes to process. We are done if (!a && !b) { - break + break; } // One of the iterators ran out of nodes. We will mark the remaining unmatched nodes in the other iterator // as deletions or additions and exit. if (!a || !b) { - break + break; } - findBestSequence(a, b) - + findBestSequence(a, b); } } -export let _context: Context; \ No newline at end of file +export let _context: Context; diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index a5eb1d9..25e48d7 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -7,85 +7,84 @@ import colorFn from "kleur"; import { NodesTable } from "./types"; export class Iterator { - ast: Node + ast: Node; nodesQueue: Node[]; lastNode!: Node; - source: string - side: Side + source: string; + side: Side; - nodesTable: NodesTable + nodesTable: NodesTable; constructor(source: string, side: Side) { - this.side = side - this.source = source - const { ast, nodesTable } = getAST(source, side) + this.side = side; + this.source = source; + const { ast, nodesTable } = getAST(source, side); this.ast = ast; - this.nodesTable = nodesTable + this.nodesTable = nodesTable; this.nodesQueue = [ast]; } next(_startFrom?: Node): Node | undefined { - let nextNode + let nextNode; while (true) { - nextNode = this._next(_startFrom) + nextNode = this._next(_startFrom); if (!nextNode) { if (this.ast.matched) { - return undefined + return undefined; } else { - return this.lastNode = this.ast + return this.lastNode = this.ast; } } if (nextNode.matched) { - return this.next(nextNode) + return this.next(nextNode); } else { this.lastNode = nextNode; - return nextNode + return nextNode; } } } _next(_startFrom?: Node): Node | undefined { - let current = _startFrom || this.lastNode + let current = _startFrom || this.lastNode; // Special case if we are in the first iteration, return the root node if (!current) { - return this.ast + return this.ast; } // Node has children, lets go to the first one if (!current.isLeafNode()) { - return current.children[0] + return current.children[0]; } // If we are in a leaf node we need to // - Go to the sibling node, if exist, or // - Go up and find a sibling node there while (true) { - const parent = current.parent - + const parent = current.parent; if (!parent) { - return + return; } // We found a node with children, we need to check if there are any sibling remaining - const currentNodeIndex = parent.children.findIndex(x => x.id === current.id) - const hasSibling = parent.children[currentNodeIndex + 1] + const currentNodeIndex = parent.children.findIndex((x) => x.id === current.id); + const hasSibling = parent.children[currentNodeIndex + 1]; if (hasSibling) { - return hasSibling + return hasSibling; } - current = current.parent! + current = current.parent!; } } resetWalkingOrder() { - this.lastNode = this.ast + this.lastNode = this.ast; } printNode(node: Node) { @@ -94,10 +93,9 @@ export class Iterator { const { start, end } = node.getRange(); - const color = node.isLeafNode() ? colorFn.magenta : colorFn.yellow + const color = node.isLeafNode() ? colorFn.magenta : colorFn.yellow; const result = getSourceWithChange(chars, start, end, color); console.log(result.join("")); } - -} \ No newline at end of file +} diff --git a/src/v2/node.ts b/src/v2/node.ts index 93402b5..5e1fa0b 100644 --- a/src/v2/node.ts +++ b/src/v2/node.ts @@ -1,46 +1,45 @@ import { getPrettyKind } from "../debug"; import { SyntaxKind } from "./types"; -import { Range } from '../types' +import { Range } from "../types"; import { Side } from "../shared/language"; import { _context } from "."; interface NewNodeArgs { - side: Side + side: Side; id: number; kind: SyntaxKind; text: string; start: number; - end: number + end: number; parent: Node | undefined; } export class Node { - side: Side + side: Side; id: number; kind: SyntaxKind; text: string; start: number; - end: number + end: number; parent: Node | undefined; - children: Node[] = [] + children: Node[] = []; - matched = false + matched = false; prettyKind: string; constructor(args: NewNodeArgs) { - const { side, id, kind, text, parent, start, end } = args + const { side, id, kind, text, parent, start, end } = args; - this.side = side + this.side = side; this.id = id; this.kind = kind; this.text = text; - this.start = start - this.end = end - this.parent = parent - - this.prettyKind = getPrettyKind(kind) + this.start = start; + this.end = end; + this.parent = parent; + this.prettyKind = getPrettyKind(kind); } isLeafNode() { @@ -63,4 +62,4 @@ export class Node { iter.printNode(this); } -} \ No newline at end of file +} diff --git a/src/v2/types.ts b/src/v2/types.ts index ed90008..54a63bc 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -1,10 +1,10 @@ -import { Node } from "./node" +import { Node } from "./node"; -export type SyntaxKind = number +export type SyntaxKind = number; -export type NodesTable = Map +export type NodesTable = Map; export interface ParsedProgram { ast: Node; nodesTable: NodesTable; -} \ No newline at end of file +} diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 6c781d3..5bbd478 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -8,5 +8,5 @@ export class Context { constructor( public sourceA: string, public sourceB: string, - ) { } + ) {} } diff --git a/tests/v2/tree-walker.test.ts b/tests/v2/tree-walker.test.ts index acdabcc..a0524ee 100644 --- a/tests/v2/tree-walker.test.ts +++ b/tests/v2/tree-walker.test.ts @@ -2,7 +2,7 @@ import { expect, test } from "vitest"; import { Iterator } from "../../src/v2/iterator"; import { Side } from "../../src/shared/language"; -const source = "1 + (2 + 3)" +const source = "1 + (2 + 3)"; const expectedNodes = [ "SourceFile", "SyntaxList", @@ -21,79 +21,79 @@ const expectedNodes = [ ]; test("Ensure the tree walker visits all nodes in order", () => { - const iter = new Iterator(source, Side.a) + const iter = new Iterator(source, Side.a); // Prime the walker so that we skip the "SourceFile", we will use that to detect when the iteration start over - const nodes: string[] = [iter.next()!.prettyKind] + const nodes: string[] = [iter.next()!.prettyKind]; while (true) { - const next = iter.next()!.prettyKind + const next = iter.next()!.prettyKind; - if (next === 'SourceFile') { - break + if (next === "SourceFile") { + break; } - nodes.push(next) + nodes.push(next); } expect(nodes).toMatchObject(expectedNodes); }); test("Ensure the tree walker visits all nodes by passing one node that the time", () => { - const iter = new Iterator(source, Side.a) + const iter = new Iterator(source, Side.a); - const nodes: string[] = [] + const nodes: string[] = []; const one = iter.next(); - nodes.push(one!.prettyKind) + nodes.push(one!.prettyKind); const two = iter.next(one); - nodes.push(two!.prettyKind) + nodes.push(two!.prettyKind); const three = iter.next(two); - nodes.push(three!.prettyKind) + nodes.push(three!.prettyKind); const four = iter.next(three); - nodes.push(four!.prettyKind) + nodes.push(four!.prettyKind); const five = iter.next(four); - nodes.push(five!.prettyKind) + nodes.push(five!.prettyKind); const six = iter.next(five); - nodes.push(six!.prettyKind) + nodes.push(six!.prettyKind); const seven = iter.next(six); - nodes.push(seven!.prettyKind) + nodes.push(seven!.prettyKind); const eight = iter.next(seven); - nodes.push(eight!.prettyKind) + nodes.push(eight!.prettyKind); const nine = iter.next(eight); - nodes.push(nine!.prettyKind) + nodes.push(nine!.prettyKind); const ten = iter.next(nine); - nodes.push(ten!.prettyKind) + nodes.push(ten!.prettyKind); const eleven = iter.next(ten); - nodes.push(eleven!.prettyKind) + nodes.push(eleven!.prettyKind); const twelve = iter.next(eleven); - nodes.push(twelve!.prettyKind) + nodes.push(twelve!.prettyKind); const thirteen = iter.next(twelve); - nodes.push(thirteen!.prettyKind) + nodes.push(thirteen!.prettyKind); const fourteen = iter.next(thirteen); - nodes.push(fourteen!.prettyKind) + nodes.push(fourteen!.prettyKind); expect(nodes).toMatchObject(expectedNodes); }); test("Ensure the tree walker loops back after visiting all the nodes", () => { - const iter = new Iterator(source, Side.a) + const iter = new Iterator(source, Side.a); let lastNode; let i = 0; while (i < expectedNodes.length) { - lastNode = iter.next() - i++ + lastNode = iter.next(); + i++; } - const shouldBeFirstNode = iter.next()!.prettyKind + const shouldBeFirstNode = iter.next()!.prettyKind; - expect(shouldBeFirstNode).toBe(expectedNodes[0]) + expect(shouldBeFirstNode).toBe(expectedNodes[0]); }); test("Ensure the tree walker ignores matched nodes", () => { - const iter = new Iterator(source, Side.a) + const iter = new Iterator(source, Side.a); // Given the full node list, lets ignore the following const expectedNodes2 = [ @@ -113,31 +113,47 @@ test("Ensure the tree walker ignores matched nodes", () => { "EndOfFileToken", ]; - const nodesToMatch = [0, 7, 11] + const nodesToMatch = [0, 7, 11]; // First pass over the node to mark a few as matched while (true) { - const node = iter.next()! + const node = iter.next()!; if (nodesToMatch.includes(node.id)) { - node.mark() + node.mark(); } - if (node.prettyKind === "EndOfFileToken") break + if (node.prettyKind === "EndOfFileToken") break; } - iter.resetWalkingOrder() + iter.resetWalkingOrder(); - const nodes = [] + const nodes = []; // Second pass to gather the non-matched nodes while (true) { - const node = iter.next()! + const node = iter.next()!; - nodes.push(node.prettyKind) + nodes.push(node.prettyKind); - if (node.prettyKind === "EndOfFileToken") break + if (node.prettyKind === "EndOfFileToken") break; } - expect(nodes).toMatchObject(expectedNodes2) + expect(nodes).toMatchObject(expectedNodes2); +}); + +test("Ensure the tree walker returns undefined after all nodes are matched", () => { + const iter = new Iterator(source, Side.a); + + while (true) { + const node = iter.next()!; + node.mark(); -}); \ No newline at end of file + if (node.prettyKind === "EndOfFileToken") break; + } + + iter.resetWalkingOrder(); + + expect(iter.next()).toBe(undefined); + expect(iter.next()).toBe(undefined); + expect(iter.next()).toBe(undefined); +}); From 6da8e8390f6a7fa120c2a2f1b3a685a904ab0773 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sun, 25 Feb 2024 19:52:13 -0300 Subject: [PATCH 09/79] wip --- src/debug.ts | 83 +++++++++++++++++++++++ src/v2/compilers.ts | 5 ++ src/v2/diff.ts | 158 +++++++++++++++++++++++++++++++++++++++++++- src/v2/index.ts | 80 ++++++++++++++++++---- src/v2/iterator.ts | 44 +++++++++++- src/v2/types.ts | 19 ++++++ src/v2/utils.ts | 2 +- 7 files changed, 373 insertions(+), 18 deletions(-) diff --git a/src/debug.ts b/src/debug.ts index 2658954..a41bfff 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -2,6 +2,9 @@ import ts from "typescript"; import Table from "cli-table3"; import { DiffType } from "./types"; import colorFn from "kleur"; +import { Sequence } from "./v2/types"; +import { getSourceWithChange } from "./backend/printer"; +import { _context as _context2 } from "./v2/index"; enum ErrorType { DebugFailure = "DebugFailure", @@ -121,3 +124,83 @@ export function getNodeForPrinting(kind: number, text: string | undefined) { text: _text, }; } + +// V2 +const COLOR_ROULETTE = [ + colorFn.red, + colorFn.green, + colorFn.yellow, + colorFn.blue, + colorFn.magenta, + colorFn.cyan, +] + +let _currentColor = -1 +export function getColor() { + _currentColor++ + + if (_currentColor >= COLOR_ROULETTE.length) { + _currentColor = 0 + } + + return COLOR_ROULETTE[_currentColor] +} + +function resetColorRoulette() { + _currentColor = 0 +} + +export function getPrettyPrintSequence(sequence: Sequence, sourceA: string, sourceB: string) { + const { iterA, iterB } = _context2 + let charsA = sourceA.split('') + let charsB = sourceB.split('') + + for (const segment of sequence.segments) { + const [indexStartA, indexEndA] = segment.a + const [indexStartB, indexEndB] = segment.b + + const startA = iterA.nodes[indexStartA].start; + const endA = iterA.nodes[indexEndA].end; + + const startB = iterB.nodes[indexStartB].start; + const endB = iterB.nodes[indexEndB].end; + + console.log("A", startA, endA) + console.log("B", startB, endB) + + // console.log("A Start", colorFn.magenta(iterA.nodes[indexStartA].prettyKind), colorFn.blue(indexStartA)) + // console.log("A End", colorFn.magenta(iterA.nodes[indexEndA].prettyKind), colorFn.blue(indexEndA)) + // console.log(getSourceWithChange(sourceA.split(''), startA, endA, colorFn.green).join('')) + + // console.log("B Start", colorFn.magenta(iterB.nodes[indexStartB].prettyKind), colorFn.blue(indexStartB)) + // console.log("B End", colorFn.magenta(iterB.nodes[indexEndB].prettyKind), colorFn.blue(indexEndB)) + // console.log(getSourceWithChange(sourceB.split(''), startB, endB, colorFn.green).join('')) + + + + const color = getColor() + + charsA = getSourceWithChange(charsA, startA, endA, color) + charsB = getSourceWithChange(charsB, startB, endB, color) + } + + resetColorRoulette() + + return { + a: charsA.join(''), + b: charsB.join('') + } +} + +export function prettyPrintSequences(a: string, b: string, sequences: Sequence[]) { + let sequenceCounter = -1 + for (const sequence of sequences) { + sequenceCounter++ + console.log(`\n---------- Starter ${sequence.starterNode.prettyKind} ${`"${sequence.starterNode.text}"` || ''} Length: ${sequence.length} Segments ${sequence.segments.length} Skips: ${sequence.skips} ----------\n`) + const sourcesWithColor = getPrettyPrintSequence(sequence, a, b) + + const table = createTextTable(sourcesWithColor.a, sourcesWithColor.b) + + console.log(table) + } +} \ No newline at end of file diff --git a/src/v2/compilers.ts b/src/v2/compilers.ts index 891fd94..0fea0d8 100644 --- a/src/v2/compilers.ts +++ b/src/v2/compilers.ts @@ -6,6 +6,8 @@ import { NodesTable, ParsedProgram } from "./types"; export function getAST(source: string, side: Side): ParsedProgram { const ast = getSourceFile(source); + const nodes: Node[] = []; + const nodesTable: NodesTable = new Map(); let i = 0; @@ -35,6 +37,8 @@ export function getAST(source: string, side: Side): ParsedProgram { }); i++; + nodes.push(newNode) + storeNodeInNodeTable(nodesTable, newNode); if (!isLeafNode) { @@ -49,6 +53,7 @@ export function getAST(source: string, side: Side): ParsedProgram { return { ast: newAst, nodesTable, + nodes }; } diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 4353029..d73c48e 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -1,5 +1,8 @@ +import { _context } from "."; import { DiffType } from "../types"; +import { range } from "../utils"; import { Node } from "./node"; +import { Sequence, Segment } from "./types"; interface ChangeSegment { type: DiffType; @@ -17,4 +20,157 @@ export class Change { } } -export function findBestSequence(a: Node, b: Node) {} +// SCORE FN PARAMETERS +const SCORE_THRESHOLD = 80 +const MAX_NODE_SKIPS = 5 + +export function getSequence(nodeA: Node, nodeB: Node): Sequence { + const segments: Segment[] = [] + const { iterA, iterB } = _context + + let bestSequence = 1; + let skips = 0; + + // Where the segment starts + let segmentAStart = nodeA.id + let segmentBStart = nodeB.id + + let indexA = nodeA.id + let indexB = nodeB.id + + mainLoop: while (true) { + // TODO-2 First iteration already has the nodes + const nextA = iterA.nextArray(indexA); + const nextB = iterB.nextArray(indexB); + + // If one of the iterators ends then there is no more search to do + if (!nextA || !nextB) { + segments.push({ + type: DiffType.move, + a: [segmentAStart, indexA - 1], + b: [segmentBStart, indexB - 1] + }) + break mainLoop; + } + + // Here two things can happen, either the match continues so we keep on advancing the cursors + if (equals(nextA, nextB)) { + bestSequence++ + indexA++ + indexB++ + continue + } + + // Or, we find a discrepancy. Before try to skip nodes to recover the match we record the current segment + segments.push({ + type: DiffType.move, + a: [segmentAStart, indexA - 1], + b: [segmentBStart, indexB - 1] + }) + + // TODO-2 This could be a source of Change type diff, maybe + // "A B C" transformed into "A b C" where "B" changed into "b" + // if (areNodesSimilar(nextA, nextB)) { + // continue + // } + + // We will try match the current B node with the following N nodes on A + + let numberOfSkips = 0 + + // Look until we reach the skip limit or the end of the iterator, whatever happens first + const lookForwardUntil = Math.min(indexA + MAX_NODE_SKIPS, iterA.nodes.length) + + // Start by skipping the current node + for (const nextIndexA of range(indexA + 1, lookForwardUntil)) { + numberOfSkips++ + + const newCandidate = iterA.peek(nextIndexA)! + + // We found a match, so we will resume the matching in a new segment from there + if (equals(newCandidate, nextB)) { + segmentAStart = nextIndexA + segmentBStart = indexB + indexA = nextIndexA + skips = numberOfSkips + continue mainLoop + } + } + + // We didn't find a candidate after advancing the cursor, we are done + break + } + + return { + starterNode: nodeB, + length: bestSequence, + skips, + segments + } + +} + +function equals(nodeOne: Node, nodeTwo: Node): boolean { + return nodeOne.kind === nodeTwo.kind && nodeOne.text === nodeTwo.text; +} + +// function areNodesSimilar(nodeA: Node, nodeB: Node) { +// const similarity = 80 + +// // TODO-2 move to options +// if (similarity <= SCORE_THRESHOLD) { +// return true +// } else { +// return false +// } +// } + +// export function findBestSequence() { +// const { iterA, iterB } = _context; + +// const sequences: Change[] = [] +// for (const node of iterA.nodes) { +// if (node.matched) { +// continue +// } + +// const candidates = iterB.getSimilarNodes(node) + +// if (candidates.length === 0) { +// // Deletion +// } + +// for (const candidate of candidates) { +// const lcs = getBestSequence(candidate, iterB) + +// sequences.push(lcs) +// } +// } +// } + +// export function getSequence(nodeA: Node, nodeB: Node): Sequence { +// const { iterA, iterB } = _context + +// let bestSequence = 1; + +// let indexA = nodeA.id +// let indexB = nodeB.id + +// while (true) { +// const nextA = iterA.peek(indexA); +// const nextB = iterB.peek(indexB); + +// if (!nextA || !nextB) { +// break; +// } + +// if (!areNodesSimilar(nextA, nextB)) { +// break; +// } + +// indexA = stepFn(indexA); +// indexB = stepFn(indexB); + +// bestSequence++; +// } +// } \ No newline at end of file diff --git a/src/v2/index.ts b/src/v2/index.ts index dff36f9..99e521b 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,7 +1,8 @@ import { Context } from "./utils"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; -import { Change, findBestSequence } from "./diff"; +import { Change, getSequence } from "./diff"; +import { Sequence } from "./types"; export function getDiff2(sourceA: string, sourceB: string) { const changes: Change[] = []; @@ -9,29 +10,80 @@ export function getDiff2(sourceA: string, sourceB: string) { const iterA = new Iterator(sourceA, Side.a); const iterB = new Iterator(sourceB, Side.b); + // console.log('------------- A ----------') + // iterA.nodes.map((x, i) => { + // console.log(i, "|", x.start, x.end, x.prettyKind, `"${x.text}"`) + // }) + + // console.log('------------- B ----------') + // iterB.nodes.map((x, i) => { + // console.log(i, "|", x.start, x.end, x.prettyKind, `"${x.text}"`) + // }) + _context = new Context(sourceA, sourceB); _context.iterA = iterA; _context.iterB = iterB; + + const sequences: Sequence[] = [] - let i = 0; - while (true) { - const a = iterA.next(); - const b = iterB.next(); + // 1- For every node on B, we look for candidates on A + for (const nodeB of iterB.nodes) { + const aSideCandidates = iterA.getMatchingNodes(nodeB) - // No more nodes to process. We are done - if (!a && !b) { - break; + if (aSideCandidates.length === 0) { + // Report missing + console.log('Missing node found', nodeB.prettyKind, nodeB.text) + continue } + + // 2- For every candidate we find their sequences and store them in the global sequence list + for (const candidate of aSideCandidates) { + const possibleSequences = getSequence(candidate, nodeB) - // One of the iterators ran out of nodes. We will mark the remaining unmatched nodes in the other iterator - // as deletions or additions and exit. - if (!a || !b) { - break; + sequences.push(possibleSequences) } - - findBestSequence(a, b); } + + return sequences + + // 3- Sort the sequences by length + // 4- For though all of them, checking if they are still applicable, if so record the move, otherwise skip it. (count matched nodes to skip early if possible) + + + + // const sortedSequences = sequences.sort((a,b) => { + // if (a.length < b.length) { + // return 1 + // } else if (a.length > b.length) { + // return -1 + // } else { + // return a.skips >= b.skips ? 1 : -1 + // } + // }) + + // return sortedSequences + + // let matchedANodes = 0 + // let matchedBNodes = 0 + // for (const seq of sortedSequences) { + // if (!stillApplicable(seq)) { + // continue + // } + + // const [matchedA, matchedB] = applySequence(seq) + + // matchedANodes += matchedA + // matchedBNodes += matchedB + + // if (iterA.nodes.length === matchedANodes) { + // // Report adds + // } + + // if (iterB.nodes.length === matchedBNodes) { + // // Report dels + // } + // } } export let _context: Context; diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index 25e48d7..78af337 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -8,8 +8,12 @@ import { NodesTable } from "./types"; export class Iterator { ast: Node; + nodes: Node[] nodesQueue: Node[]; lastNode!: Node; + + lastNodeVisited!: Node; + source: string; side: Side; @@ -18,7 +22,9 @@ export class Iterator { constructor(source: string, side: Side) { this.side = side; this.source = source; - const { ast, nodesTable } = getAST(source, side); + const { ast, nodesTable, nodes } = getAST(source, side); + + this.nodes = nodes this.ast = ast; this.nodesTable = nodesTable; @@ -26,7 +32,31 @@ export class Iterator { this.nodesQueue = [ast]; } - next(_startFrom?: Node): Node | undefined { + // Get the next unmatched node in the iterator, optionally after a given index + nextArray(index?: number) { + const start = index ?? 0 + for (let i = start; i < this.nodes.length; i++) { + const node = this.nodes[i]; + + if (node.matched) { + continue; + } + + return this.lastNodeVisited = node; + } + } + + peek(index: number) { + const item = this.nodes[index]; + + if (!item || item.matched) { + return; + } + + return item; + } + + next(_startFrom?: Node, startOver = false): Node | undefined { let nextNode; while (true) { nextNode = this._next(_startFrom); @@ -87,6 +117,16 @@ export class Iterator { this.lastNode = this.ast; } + getMatchingNodes(targetNode: Node) { + const rawCandidates = this.nodesTable.get(targetNode.kind); + + if (!rawCandidates) { + return []; + } + + return rawCandidates.filter(x => x.text === targetNode.text); + } + printNode(node: Node) { const source = this.side === Side.a ? _context.sourceA : _context.sourceB; const chars = source.split(""); diff --git a/src/v2/types.ts b/src/v2/types.ts index 54a63bc..9cba384 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -1,3 +1,4 @@ +import { DiffType } from "../types"; import { Node } from "./node"; export type SyntaxKind = number; @@ -6,5 +7,23 @@ export type NodesTable = Map; export interface ParsedProgram { ast: Node; + nodes: Node[] nodesTable: NodesTable; } + +// Start is inclusive, end is not inclusive +type SegmentRange = [startIndex: number, endIndex: number] + +export interface Segment { + type: DiffType; + a: SegmentRange; + b: SegmentRange; +} + +// TODO: Use a better way to store this +export interface Sequence { + starterNode: Node; + length: number; + skips: number; + segments: Segment[] +} \ No newline at end of file diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 5bbd478..096a719 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -9,4 +9,4 @@ export class Context { public sourceA: string, public sourceB: string, ) {} -} +} \ No newline at end of file From 4e1423d64bef243139f135e852972be32307287b Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sun, 25 Feb 2024 21:10:08 -0300 Subject: [PATCH 10/79] Working --- src/backend/printer.ts | 16 ++++++++- src/debug.ts | 39 +++++++++------------- src/v2/compilers.ts | 42 ++++++++++++++++------- src/v2/diff.ts | 12 +++---- src/v2/iterator.ts | 75 +++--------------------------------------- src/v2/node.ts | 21 +++++++----- src/v2/types.ts | 4 +-- 7 files changed, 84 insertions(+), 125 deletions(-) diff --git a/src/backend/printer.ts b/src/backend/printer.ts index c79fd15..c3b9e59 100644 --- a/src/backend/printer.ts +++ b/src/backend/printer.ts @@ -85,7 +85,7 @@ export function getSourceWithChange( const head = chars.slice(0, start); - const text = chars.slice(start, end).join(""); + let text = chars.slice(start, end).join(""); const tail = chars.slice(end, chars.length); @@ -100,6 +100,20 @@ export function getSourceWithChange( const compliment = getComplimentArray(charsToAdd); + // Since we might display the changes in a table, where we split by newlines, simply coloring the whole segment won't work, for example: + // + // (COLOR_START) a + // b (COLOR END) + // + // When splitted you will get ["(COLOR_START) a", " b (COLOR END)"] where only the first line would be colored, not the second + // So to fix it we wrap all the tokens individually + // + // (COLOR_START) a (COLOR END) + // (COLOR_START) b (COLOR END) + // + // So that when splitted you get ["(COLOR_START) a (COLOR END)", "(COLOR_START) b (COLOR END)"] which produces the desired output + text = text.split(' ').map(x => colorFn(x)).join(' ') + return [...head, colorFn(text), ...compliment, ...tail]; } diff --git a/src/debug.ts b/src/debug.ts index a41bfff..3fbc98e 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -2,9 +2,10 @@ import ts from "typescript"; import Table from "cli-table3"; import { DiffType } from "./types"; import colorFn from "kleur"; -import { Sequence } from "./v2/types"; +import { SegmentRange, Sequence } from "./v2/types"; import { getSourceWithChange } from "./backend/printer"; import { _context as _context2 } from "./v2/index"; +import { Iterator } from './v2/iterator' enum ErrorType { DebugFailure = "DebugFailure", @@ -150,33 +151,23 @@ function resetColorRoulette() { _currentColor = 0 } +function getStringPositionsFromRange(iter: Iterator, range: SegmentRange): [start: number, end: number] { + const [indexStart, indexEnd] = range + + const start = iter.nodes[indexStart].start; + const end = iter.nodes[indexEnd].end; + + return [start, end] +} + export function getPrettyPrintSequence(sequence: Sequence, sourceA: string, sourceB: string) { const { iterA, iterB } = _context2 let charsA = sourceA.split('') let charsB = sourceB.split('') for (const segment of sequence.segments) { - const [indexStartA, indexEndA] = segment.a - const [indexStartB, indexEndB] = segment.b - - const startA = iterA.nodes[indexStartA].start; - const endA = iterA.nodes[indexEndA].end; - - const startB = iterB.nodes[indexStartB].start; - const endB = iterB.nodes[indexEndB].end; - - console.log("A", startA, endA) - console.log("B", startB, endB) - - // console.log("A Start", colorFn.magenta(iterA.nodes[indexStartA].prettyKind), colorFn.blue(indexStartA)) - // console.log("A End", colorFn.magenta(iterA.nodes[indexEndA].prettyKind), colorFn.blue(indexEndA)) - // console.log(getSourceWithChange(sourceA.split(''), startA, endA, colorFn.green).join('')) - - // console.log("B Start", colorFn.magenta(iterB.nodes[indexStartB].prettyKind), colorFn.blue(indexStartB)) - // console.log("B End", colorFn.magenta(iterB.nodes[indexEndB].prettyKind), colorFn.blue(indexEndB)) - // console.log(getSourceWithChange(sourceB.split(''), startB, endB, colorFn.green).join('')) - - + const [startA, endA] = getStringPositionsFromRange(iterA, segment.a) + const [startB, endB] = getStringPositionsFromRange(iterB, segment.b) const color = getColor() @@ -184,8 +175,6 @@ export function getPrettyPrintSequence(sequence: Sequence, sourceA: string, sour charsB = getSourceWithChange(charsB, startB, endB, color) } - resetColorRoulette() - return { a: charsA.join(''), b: charsB.join('') @@ -202,5 +191,7 @@ export function prettyPrintSequences(a: string, b: string, sequences: Sequence[] const table = createTextTable(sourcesWithColor.a, sourcesWithColor.b) console.log(table) + + resetColorRoulette() } } \ No newline at end of file diff --git a/src/v2/compilers.ts b/src/v2/compilers.ts index 0fea0d8..9c02dc2 100644 --- a/src/v2/compilers.ts +++ b/src/v2/compilers.ts @@ -1,12 +1,14 @@ -import { Node as TsNode } from "typescript"; +import { SyntaxKind, Node as TsNode } from "typescript"; import { Node } from "./node"; import { getSourceFile } from "../frontend/utils"; import { Side } from "../shared/language"; import { NodesTable, ParsedProgram } from "./types"; -export function getAST(source: string, side: Side): ParsedProgram { +export function getTsNodes(source: string, side: Side): ParsedProgram { const ast = getSourceFile(source); + const nodes: Node[] = []; + const allNodes: Node[] = []; const nodesTable: NodesTable = new Map(); @@ -25,38 +27,54 @@ export function getAST(source: string, side: Side): ParsedProgram { const start = node.pos + node.getLeadingTriviaWidth(); const end = node.end; + const isTextNode = verifyTextNode(node) + const newNode = new Node({ side, - id: i, + index: nodes.length, + globalIndex: i, kind: node.kind, // In typescript a non-leaf node contains as string the whole children text combined, so we ignore it - text: isLeafNode ? node.getText() : "", + text: isTextNode ? node.getText() : "", start, end, parent, + isTextNode }); i++; - nodes.push(newNode) + allNodes.push(newNode) - storeNodeInNodeTable(nodesTable, newNode); - - if (!isLeafNode) { - newNode.children = node.getChildren().map((x) => walk(x, newNode)); + if (isTextNode) { + nodes.push(newNode) } + + + storeNodeInNodeTable(nodesTable, newNode); + + node.getChildren().map((x) => walk(x, newNode)); + return newNode; } - const newAst = walk(ast, undefined); + walk(ast, undefined); return { - ast: newAst, nodesTable, - nodes + nodes, + allNodes }; } + +function verifyTextNode(node: TsNode) { + const isReservedWord = node.kind >= SyntaxKind.FirstKeyword && node.kind <= SyntaxKind.LastKeyword; + const isPunctuation = node.kind >= SyntaxKind.FirstPunctuation && node.kind <= SyntaxKind.LastPunctuation; + + return (node as any).text && node.kind !== SyntaxKind.SourceFile || isReservedWord || isPunctuation +} + function storeNodeInNodeTable(nodesTable: NodesTable, node: Node) { const currentValue = nodesTable.get(node.kind); diff --git a/src/v2/diff.ts b/src/v2/diff.ts index d73c48e..78040de 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -32,16 +32,16 @@ export function getSequence(nodeA: Node, nodeB: Node): Sequence { let skips = 0; // Where the segment starts - let segmentAStart = nodeA.id - let segmentBStart = nodeB.id + let segmentAStart = nodeA.index + let segmentBStart = nodeB.index - let indexA = nodeA.id - let indexB = nodeB.id + let indexA = nodeA.index + let indexB = nodeB.index mainLoop: while (true) { // TODO-2 First iteration already has the nodes - const nextA = iterA.nextArray(indexA); - const nextB = iterB.nextArray(indexB); + const nextA = iterA.next(indexA); + const nextB = iterB.next(indexB); // If one of the iterators ends then there is no more search to do if (!nextA || !nextB) { diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index 78af337..67f612b 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -1,16 +1,13 @@ import { _context } from "."; import { getSourceWithChange } from "../backend/printer"; import { Side } from "../shared/language"; -import { getAST } from "./compilers"; +import { getTsNodes } from "./compilers"; import { Node } from "./node"; import colorFn from "kleur"; import { NodesTable } from "./types"; export class Iterator { - ast: Node; nodes: Node[] - nodesQueue: Node[]; - lastNode!: Node; lastNodeVisited!: Node; @@ -22,18 +19,15 @@ export class Iterator { constructor(source: string, side: Side) { this.side = side; this.source = source; - const { ast, nodesTable, nodes } = getAST(source, side); + const { nodesTable, nodes } = getTsNodes(source, side); this.nodes = nodes - this.ast = ast; this.nodesTable = nodesTable; - - this.nodesQueue = [ast]; } // Get the next unmatched node in the iterator, optionally after a given index - nextArray(index?: number) { + next(index?: number) { const start = index ?? 0 for (let i = start; i < this.nodes.length; i++) { const node = this.nodes[i]; @@ -56,67 +50,6 @@ export class Iterator { return item; } - next(_startFrom?: Node, startOver = false): Node | undefined { - let nextNode; - while (true) { - nextNode = this._next(_startFrom); - - if (!nextNode) { - if (this.ast.matched) { - return undefined; - } else { - return this.lastNode = this.ast; - } - } - - if (nextNode.matched) { - return this.next(nextNode); - } else { - this.lastNode = nextNode; - return nextNode; - } - } - } - - _next(_startFrom?: Node): Node | undefined { - let current = _startFrom || this.lastNode; - - // Special case if we are in the first iteration, return the root node - if (!current) { - return this.ast; - } - - // Node has children, lets go to the first one - if (!current.isLeafNode()) { - return current.children[0]; - } - - // If we are in a leaf node we need to - // - Go to the sibling node, if exist, or - // - Go up and find a sibling node there - while (true) { - const parent = current.parent; - - if (!parent) { - return; - } - - // We found a node with children, we need to check if there are any sibling remaining - const currentNodeIndex = parent.children.findIndex((x) => x.id === current.id); - const hasSibling = parent.children[currentNodeIndex + 1]; - - if (hasSibling) { - return hasSibling; - } - - current = current.parent!; - } - } - - resetWalkingOrder() { - this.lastNode = this.ast; - } - getMatchingNodes(targetNode: Node) { const rawCandidates = this.nodesTable.get(targetNode.kind); @@ -133,7 +66,7 @@ export class Iterator { const { start, end } = node.getRange(); - const color = node.isLeafNode() ? colorFn.magenta : colorFn.yellow; + const color = node.isTextNode ? colorFn.magenta : colorFn.yellow; const result = getSourceWithChange(chars, start, end, color); console.log(result.join("")); diff --git a/src/v2/node.ts b/src/v2/node.ts index 5e1fa0b..565c164 100644 --- a/src/v2/node.ts +++ b/src/v2/node.ts @@ -5,7 +5,10 @@ import { Side } from "../shared/language"; import { _context } from "."; interface NewNodeArgs { side: Side; - id: number; + + index: number; + globalIndex: number; + kind: SyntaxKind; text: string; @@ -13,39 +16,39 @@ interface NewNodeArgs { end: number; parent: Node | undefined; + isTextNode: boolean } export class Node { side: Side; - id: number; + index: number; + globalIndex: number; kind: SyntaxKind; text: string; start: number; end: number; parent: Node | undefined; - children: Node[] = []; + isTextNode: boolean; matched = false; prettyKind: string; constructor(args: NewNodeArgs) { - const { side, id, kind, text, parent, start, end } = args; + const { side, index, globalIndex, kind, text, parent, start, end, isTextNode } = args; this.side = side; - this.id = id; + this.index = index; + this.globalIndex = globalIndex; this.kind = kind; this.text = text; this.start = start; this.end = end; this.parent = parent; + this.isTextNode = isTextNode this.prettyKind = getPrettyKind(kind); } - isLeafNode() { - return this.children.length === 0; - } - getRange(): Range { return { start: this.start, diff --git a/src/v2/types.ts b/src/v2/types.ts index 9cba384..fe6b5bd 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -6,13 +6,13 @@ export type SyntaxKind = number; export type NodesTable = Map; export interface ParsedProgram { - ast: Node; nodes: Node[] + allNodes: Node[] nodesTable: NodesTable; } // Start is inclusive, end is not inclusive -type SegmentRange = [startIndex: number, endIndex: number] +export type SegmentRange = [startIndex: number, endIndex: number] export interface Segment { type: DiffType; From ff579fca15a215cdd2986594b13d0611c24ce32f Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 1 Mar 2024 16:16:08 -0300 Subject: [PATCH 11/79] Super wip --- deno.json | 1 - package.json | 2 +- src/backend/printer.ts | 8 +- src/debug.ts | 119 ++++++----- src/v2/compilers.ts | 21 +- src/v2/core.ts | 86 ++++++++ src/v2/diff.ts | 197 +++++++++---------- src/v2/index.ts | 118 ++++++----- src/v2/iterator.ts | 29 ++- src/v2/node.ts | 9 +- src/v2/types.ts | 21 +- src/v2/utils.ts | 47 ++++- tests/conformance/additions-removals.test.ts | 28 +-- tests/conformance/alignment.test.ts | 168 ++++++++-------- tests/conformance/changes.test.ts | 24 +-- tests/conformance/move.test.ts | 84 ++++---- tests/conformance/sequence-matching.test.ts | 71 ++++--- tests/conformance/trivia-format.test.ts | 117 ++++++----- tests/lcs.test.ts | 2 +- tests/realWorld/case1/a.ts | 2 +- tests/realWorld/case1/b.ts | 4 +- tests/realWorld/case2/a.ts | 2 +- tests/realWorld/case3/a.ts | 2 +- tests/realWorld/realWordRunner.test.ts | 48 ++--- tests/unitTests/serializer.test.ts | 38 ++-- tests/utils.ts | 40 ++-- tsconfig.json | 5 +- 27 files changed, 726 insertions(+), 567 deletions(-) create mode 100644 src/v2/core.ts diff --git a/deno.json b/deno.json index 8f02aeb..dde0686 100644 --- a/deno.json +++ b/deno.json @@ -2,7 +2,6 @@ "exclude": [ "ui", "scripts", - "tests", "node_modules", "internal", "dist", diff --git a/package.json b/package.json index 6659624..e30e8ec 100644 --- a/package.json +++ b/package.json @@ -37,4 +37,4 @@ "vite": "4.4.2", "vitest": "0.33.0" } -} \ No newline at end of file +} diff --git a/src/backend/printer.ts b/src/backend/printer.ts index c3b9e59..af9d49d 100644 --- a/src/backend/printer.ts +++ b/src/backend/printer.ts @@ -101,18 +101,18 @@ export function getSourceWithChange( const compliment = getComplimentArray(charsToAdd); // Since we might display the changes in a table, where we split by newlines, simply coloring the whole segment won't work, for example: - // + // // (COLOR_START) a // b (COLOR END) // // When splitted you will get ["(COLOR_START) a", " b (COLOR END)"] where only the first line would be colored, not the second - // So to fix it we wrap all the tokens individually - // + // So to fix it we wrap all the tokens individually + // // (COLOR_START) a (COLOR END) // (COLOR_START) b (COLOR END) // // So that when splitted you get ["(COLOR_START) a (COLOR END)", "(COLOR_START) b (COLOR END)"] which produces the desired output - text = text.split(' ').map(x => colorFn(x)).join(' ') + text = text.split(" ").map((x) => colorFn(x)).join(" "); return [...head, colorFn(text), ...compliment, ...tail]; } diff --git a/src/debug.ts b/src/debug.ts index 3fbc98e..691eb41 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -2,10 +2,11 @@ import ts from "typescript"; import Table from "cli-table3"; import { DiffType } from "./types"; import colorFn from "kleur"; -import { SegmentRange, Sequence } from "./v2/types"; import { getSourceWithChange } from "./backend/printer"; import { _context as _context2 } from "./v2/index"; -import { Iterator } from './v2/iterator' +import { Iterator } from "./v2/iterator"; +import { getIndexesFromSegment } from "./v2/utils"; +import { Change } from "./v2/diff"; enum ErrorType { DebugFailure = "DebugFailure", @@ -14,12 +15,26 @@ enum ErrorType { } class BaseError { - constructor(public type: ErrorType, public message: string, public serializedError?: string, public extra?: unknown) {} + constructor( + public type: ErrorType, + public message: string, + public serializedError?: string, + public extra?: unknown, + ) {} } class DebugFailure extends BaseError { - constructor(public message: string, public error?: Error, public extra?: unknown) { - super(ErrorType.DebugFailure, message, error ? JSON.stringify(error) : undefined, extra); + constructor( + public message: string, + public error?: Error, + public extra?: unknown, + ) { + super( + ErrorType.DebugFailure, + message, + error ? JSON.stringify(error) : undefined, + extra, + ); } } @@ -29,7 +44,10 @@ export function fail(errorMessage?: string): never { // It receives a function instead of a raw string so that the content gets evaluated lazily. Without this, an error message that // uses the function `getPrettyKind` will trigger it independently if the assertion passes of not -export function assert(condition: T, errorMessage?: () => string): asserts condition is NonNullable { +export function assert( + condition: T, + errorMessage?: () => string, +): asserts condition is NonNullable { if (!condition) { fail(errorMessage?.()); } @@ -71,7 +89,11 @@ export function createTextTable( const maxLength = Math.max(aLines.length, bLines.length); const table = new Table({ - head: [colorFn.yellow("Nº"), colorFn.red("Source"), colorFn.green("Revision")], + head: [ + colorFn.yellow("Nº"), + colorFn.red("Source"), + colorFn.green("Revision"), + ], colAligns: ["left", "left"], colWidths: [5, 30, 30], style: { @@ -134,64 +156,73 @@ const COLOR_ROULETTE = [ colorFn.blue, colorFn.magenta, colorFn.cyan, -] +]; -let _currentColor = -1 +let _currentColor = -1; export function getColor() { - _currentColor++ + _currentColor++; if (_currentColor >= COLOR_ROULETTE.length) { - _currentColor = 0 + _currentColor = 0; } - return COLOR_ROULETTE[_currentColor] + return COLOR_ROULETTE[_currentColor]; } function resetColorRoulette() { - _currentColor = 0 + _currentColor = 0; } -function getStringPositionsFromRange(iter: Iterator, range: SegmentRange): [start: number, end: number] { - const [indexStart, indexEnd] = range - +function getStringPositionsFromRange( + iter: Iterator, + indexStart: number, + indexEnd: number, +): [start: number, end: number] { const start = iter.nodes[indexStart].start; const end = iter.nodes[indexEnd].end; - return [start, end] + return [start, end]; } -export function getPrettyPrintSequence(sequence: Sequence, sourceA: string, sourceB: string) { - const { iterA, iterB } = _context2 - let charsA = sourceA.split('') - let charsB = sourceB.split('') +export function getPrettyStringFromChange( + change: Change, + sourceA: string, + sourceB: string, +) { + const { iterA, iterB } = _context2; + let charsA = sourceA.split(""); + let charsB = sourceB.split(""); + + for (const segment of change.segments) { + const { a, b } = getIndexesFromSegment(segment); + const [startA, endA] = getStringPositionsFromRange(iterA, a.start, a.end); + const [startB, endB] = getStringPositionsFromRange(iterB, b.start, b.end); - for (const segment of sequence.segments) { - const [startA, endA] = getStringPositionsFromRange(iterA, segment.a) - const [startB, endB] = getStringPositionsFromRange(iterB, segment.b) - - const color = getColor() + const color = getColor(); - charsA = getSourceWithChange(charsA, startA, endA, color) - charsB = getSourceWithChange(charsB, startB, endB, color) + charsA = getSourceWithChange(charsA, startA, endA, color); + charsB = getSourceWithChange(charsB, startB, endB, color); } return { - a: charsA.join(''), - b: charsB.join('') - } + a: charsA.join(""), + b: charsB.join(""), + }; } -export function prettyPrintSequences(a: string, b: string, sequences: Sequence[]) { - let sequenceCounter = -1 - for (const sequence of sequences) { - sequenceCounter++ - console.log(`\n---------- Starter ${sequence.starterNode.prettyKind} ${`"${sequence.starterNode.text}"` || ''} Length: ${sequence.length} Segments ${sequence.segments.length} Skips: ${sequence.skips} ----------\n`) - const sourcesWithColor = getPrettyPrintSequence(sequence, a, b) - - const table = createTextTable(sourcesWithColor.a, sourcesWithColor.b) - - console.log(table) - - resetColorRoulette() +export function prettyPrintChanges(a: string, b: string, changes: Change[]) { + let sequenceCounter = -1; + for (const change of changes) { + sequenceCounter++; + console.log( + `\n---------- Starter ${change.startNode.prettyKind} ${`"${change.startNode.text}"` || ""} Length: ${change.length} Segments ${change.segments.length} Skips: ${change.skips} ----------\n`, + ); + const sourcesWithColor = getPrettyStringFromChange(change, a, b); + + const table = createTextTable(sourcesWithColor.a, sourcesWithColor.b); + + console.log(table); + + resetColorRoulette(); } -} \ No newline at end of file +} diff --git a/src/v2/compilers.ts b/src/v2/compilers.ts index 9c02dc2..8ab2b50 100644 --- a/src/v2/compilers.ts +++ b/src/v2/compilers.ts @@ -1,4 +1,4 @@ -import { SyntaxKind, Node as TsNode } from "typescript"; +import { Node as TsNode, SyntaxKind } from "typescript"; import { Node } from "./node"; import { getSourceFile } from "../frontend/utils"; import { Side } from "../shared/language"; @@ -27,7 +27,7 @@ export function getTsNodes(source: string, side: Side): ParsedProgram { const start = node.pos + node.getLeadingTriviaWidth(); const end = node.end; - const isTextNode = verifyTextNode(node) + const isTextNode = verifyTextNode(node); const newNode = new Node({ side, @@ -39,22 +39,20 @@ export function getTsNodes(source: string, side: Side): ParsedProgram { start, end, parent, - isTextNode + isTextNode, }); i++; - allNodes.push(newNode) + allNodes.push(newNode); if (isTextNode) { - nodes.push(newNode) + nodes.push(newNode); } - - storeNodeInNodeTable(nodesTable, newNode); - + node.getChildren().map((x) => walk(x, newNode)); - + return newNode; } @@ -63,16 +61,15 @@ export function getTsNodes(source: string, side: Side): ParsedProgram { return { nodesTable, nodes, - allNodes + allNodes, }; } - function verifyTextNode(node: TsNode) { const isReservedWord = node.kind >= SyntaxKind.FirstKeyword && node.kind <= SyntaxKind.LastKeyword; const isPunctuation = node.kind >= SyntaxKind.FirstPunctuation && node.kind <= SyntaxKind.LastPunctuation; - return (node as any).text && node.kind !== SyntaxKind.SourceFile || isReservedWord || isPunctuation + return (node as any).text && node.kind !== SyntaxKind.SourceFile || isReservedWord || isPunctuation; } function storeNodeInNodeTable(nodesTable: NodesTable, node: Node) { diff --git a/src/v2/core.ts b/src/v2/core.ts new file mode 100644 index 0000000..a4a1c9d --- /dev/null +++ b/src/v2/core.ts @@ -0,0 +1,86 @@ +import { _context } from "."; +import { SyntaxKind } from "./types"; +import { Node } from "./node"; +import { CandidateMatch, Change, findSegmentLength, Segment } from "./diff"; +import { getAllNodesFromMatch } from "./utils"; +import { fail } from "../debug"; +import { Iterator } from "./iterator"; +import { DiffType } from "../types"; + +/** + * Returns the longest possible match for the given node, this is including possible skips to improve the match length. + */ +export function getBestMatch(nodeB: Node): CandidateMatch | undefined { + const aSideCandidates = _context.iterA.getMatchingNodes(nodeB); + + if (aSideCandidates.length === 0) { + fail("WTF2"); + } + + let bestMatch: CandidateMatch = { + length: 0, + skips: 0, + segments: [], + }; + + for (const candidate of aSideCandidates) { + const currentMatch = findSegmentLength(candidate, nodeB); + + if (currentMatch.length > bestMatch.length) { + bestMatch = currentMatch; + } + } + + return bestMatch; +} + +export function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { + const nodesInMatch = getAllNodesFromMatch(match); + + const kinds = new Set(); + + for (const node of nodesInMatch) { + kinds.add(node.kind); + } + + const allNodes: Node[] = []; + for (const kind of kinds) { + const nodesOfKind = _context.iterB.nodesTable.get(kind); + + if (!nodesOfKind) { + fail(); + } + allNodes.push(...nodesOfKind); + } + + const indexOfStartNode = allNodes.findIndex((x) => x.index === starterNode.index); + allNodes.splice(indexOfStartNode, 1); + + return allNodes; +} + +export function oneSidedIteration( + iter: Iterator, + typeOfChange: DiffType.addition | DiffType.deletion, + startFrom: number, +): Change[] { + const changes: Change[] = []; + + let node = iter.next(startFrom); + + // TODO: Compactar? + while (node) { + let change: Change; + + if (typeOfChange === DiffType.addition) { + change = Change.createAddition(node); + } else { + change = Change.createDeletion(node); + } + + iter.mark(node.index, typeOfChange); + node = iter.next(node.index + 1); + } + + return changes; +} diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 78040de..b083f3a 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -1,42 +1,85 @@ import { _context } from "."; +import { assert } from "../debug"; +import { Side } from "../shared/language"; import { DiffType } from "../types"; import { range } from "../utils"; import { Node } from "./node"; -import { Sequence, Segment } from "./types"; +import { getIndexesFromSegment } from "./utils"; -interface ChangeSegment { - type: DiffType; - start: Node; - end: Node; +// Start is inclusive, end is not inclusive +export type Segment = [indexA: number, indexB: number, length: number]; + +export interface CandidateMatch { + length: number; + skips: number; + segments: Segment[]; } export class Change { - type: DiffType; - segments: ChangeSegment[]; + startNode: Node; + length: number; + constructor( + public type: DiffType, + public segments: Segment[], + public skips = 0, + ) { + const { b } = getIndexesFromSegment(segments[0]); + this.startNode = _context.iterA.nodes[b.start]; + this.length = calculateLength(segments); + } + + static createAddition(node: Node) { + assert(node.side === Side.b); + return new Change(DiffType.addition, [ + [ + // Start A + -1, + // Start B + node.index, + // Length + 1, + ], + ]); + } + + static createDeletion(node: Node) { + assert(node.side === Side.a); + return new Change(DiffType.addition, [ + [ + // Start A + node.index, + // Start B + -1, + // Length + 1, + ], + ]); + } - constructor(type: DiffType, segments: ChangeSegment[]) { - this.type = type; - this.segments = segments; + static createMove(match: CandidateMatch) { + return new Change(DiffType.move, match.segments, match.skips); } } // SCORE FN PARAMETERS -const SCORE_THRESHOLD = 80 -const MAX_NODE_SKIPS = 5 +const MAX_NODE_SKIPS = 5; -export function getSequence(nodeA: Node, nodeB: Node): Sequence { - const segments: Segment[] = [] - const { iterA, iterB } = _context +export function findSegmentLength( + nodeA: Node, + nodeB: Node, +): { segments: Segment[]; skips: number; length: number } { + const segments: Segment[] = []; + const { iterA, iterB } = _context; let bestSequence = 1; let skips = 0; // Where the segment starts - let segmentAStart = nodeA.index - let segmentBStart = nodeB.index + let segmentAStart = nodeA.index; + let segmentBStart = nodeB.index; - let indexA = nodeA.index - let indexB = nodeB.index + let indexA = nodeA.index; + let indexB = nodeB.index; mainLoop: while (true) { // TODO-2 First iteration already has the nodes @@ -45,30 +88,22 @@ export function getSequence(nodeA: Node, nodeB: Node): Sequence { // If one of the iterators ends then there is no more search to do if (!nextA || !nextB) { - segments.push({ - type: DiffType.move, - a: [segmentAStart, indexA - 1], - b: [segmentBStart, indexB - 1] - }) + segments.push([segmentAStart, segmentBStart, bestSequence]); break mainLoop; } // Here two things can happen, either the match continues so we keep on advancing the cursors if (equals(nextA, nextB)) { - bestSequence++ - indexA++ - indexB++ - continue + bestSequence++; + indexA++; + indexB++; + continue; } // Or, we find a discrepancy. Before try to skip nodes to recover the match we record the current segment - segments.push({ - type: DiffType.move, - a: [segmentAStart, indexA - 1], - b: [segmentBStart, indexB - 1] - }) + segments.push([segmentAStart, segmentBStart, bestSequence]); - // TODO-2 This could be a source of Change type diff, maybe + // TODO-2 This could be a source of Change type diff, maybe // "A B C" transformed into "A b C" where "B" changed into "b" // if (areNodesSimilar(nextA, nextB)) { // continue @@ -76,101 +111,51 @@ export function getSequence(nodeA: Node, nodeB: Node): Sequence { // We will try match the current B node with the following N nodes on A - let numberOfSkips = 0 + let numberOfSkips = 0; // Look until we reach the skip limit or the end of the iterator, whatever happens first - const lookForwardUntil = Math.min(indexA + MAX_NODE_SKIPS, iterA.nodes.length) + const lookForwardUntil = Math.min( + indexA + MAX_NODE_SKIPS, + iterA.nodes.length, + ); // Start by skipping the current node for (const nextIndexA of range(indexA + 1, lookForwardUntil)) { - numberOfSkips++ + numberOfSkips++; - const newCandidate = iterA.peek(nextIndexA)! + const newCandidate = iterA.peek(nextIndexA)!; // We found a match, so we will resume the matching in a new segment from there if (equals(newCandidate, nextB)) { - segmentAStart = nextIndexA - segmentBStart = indexB - indexA = nextIndexA - skips = numberOfSkips - continue mainLoop + segmentAStart = nextIndexA; + segmentBStart = indexB; + indexA = nextIndexA; + skips = numberOfSkips; + continue mainLoop; } } // We didn't find a candidate after advancing the cursor, we are done - break + break; } return { - starterNode: nodeB, length: bestSequence, skips, - segments - } - + segments, + }; } function equals(nodeOne: Node, nodeTwo: Node): boolean { return nodeOne.kind === nodeTwo.kind && nodeOne.text === nodeTwo.text; } -// function areNodesSimilar(nodeA: Node, nodeB: Node) { -// const similarity = 80 - -// // TODO-2 move to options -// if (similarity <= SCORE_THRESHOLD) { -// return true -// } else { -// return false -// } -// } - -// export function findBestSequence() { -// const { iterA, iterB } = _context; - -// const sequences: Change[] = [] -// for (const node of iterA.nodes) { -// if (node.matched) { -// continue -// } - -// const candidates = iterB.getSimilarNodes(node) +function calculateLength(segments: Segment[]) { + let sum = 0; -// if (candidates.length === 0) { -// // Deletion -// } - -// for (const candidate of candidates) { -// const lcs = getBestSequence(candidate, iterB) - -// sequences.push(lcs) -// } -// } -// } - -// export function getSequence(nodeA: Node, nodeB: Node): Sequence { -// const { iterA, iterB } = _context - -// let bestSequence = 1; - -// let indexA = nodeA.id -// let indexB = nodeB.id - -// while (true) { -// const nextA = iterA.peek(indexA); -// const nextB = iterB.peek(indexB); - -// if (!nextA || !nextB) { -// break; -// } - -// if (!areNodesSimilar(nextA, nextB)) { -// break; -// } - -// indexA = stepFn(indexA); -// indexB = stepFn(indexB); + for (const segment of segments) { + sum += segment[2]; + } -// bestSequence++; -// } -// } \ No newline at end of file + return sum; +} diff --git a/src/v2/index.ts b/src/v2/index.ts index 99e521b..062c4ba 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,12 +1,14 @@ -import { Context } from "./utils"; +import { Context, getIndexesFromSegment } from "./utils"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; -import { Change, getSequence } from "./diff"; -import { Sequence } from "./types"; +import { getBestMatch, getSubSequenceNodes, oneSidedIteration } from "./core"; +import { Change, Segment } from "./diff"; +import { DiffType } from "../types"; +import { fail } from "../debug"; +import { range } from "../utils"; +import { Diff } from "../data_structures/diff"; export function getDiff2(sourceA: string, sourceB: string) { - const changes: Change[] = []; - const iterA = new Iterator(sourceA, Side.a); const iterB = new Iterator(sourceB, Side.b); @@ -24,66 +26,84 @@ export function getDiff2(sourceA: string, sourceB: string) { _context.iterA = iterA; _context.iterB = iterB; - - const sequences: Sequence[] = [] - // 1- For every node on B, we look for candidates on A - for (const nodeB of iterB.nodes) { - const aSideCandidates = iterA.getMatchingNodes(nodeB) + const changes: Change[] = []; - if (aSideCandidates.length === 0) { - // Report missing - console.log('Missing node found', nodeB.prettyKind, nodeB.text) - continue + while (true) { + const a = iterA.next(); + const b = iterB.next(); + + // No more nodes to process. We are done + if (!a && !b) { + break; } - - // 2- For every candidate we find their sequences and store them in the global sequence list - for (const candidate of aSideCandidates) { - const possibleSequences = getSequence(candidate, nodeB) - sequences.push(possibleSequences) + // One of the iterators ran out of nodes. We will mark the remaining unmatched nodes + if (!a || !b) { + // If A finished means that B still have nodes, they are additions. If B finished means that A have nodes, they are deletions + const iterOn = !a ? iterB : iterA; + const type = !a ? DiffType.addition : DiffType.deletion; + const startFrom = !a ? b?.index : a.index; + + const remainingChanges = oneSidedIteration(iterOn, type, startFrom!); + changes.push(...remainingChanges); + break; } - } - return sequences + // 1- + const bestMatchForB = getBestMatch(b); - // 3- Sort the sequences by length - // 4- For though all of them, checking if they are still applicable, if so record the move, otherwise skip it. (count matched nodes to skip early if possible) + if (!bestMatchForB) { + const addition = Change.createAddition(b); + changes.push(addition); + iterB.mark(b.index, DiffType.addition); + continue; + } + const subSequenceNodesToCheck = getSubSequenceNodes(bestMatchForB, b); - // const sortedSequences = sequences.sort((a,b) => { - // if (a.length < b.length) { - // return 1 - // } else if (a.length > b.length) { - // return -1 - // } else { - // return a.skips >= b.skips ? 1 : -1 - // } - // }) + if (!subSequenceNodesToCheck.length) { + fail("IDK"); + } - // return sortedSequences + let bestCandidate = bestMatchForB; - // let matchedANodes = 0 - // let matchedBNodes = 0 - // for (const seq of sortedSequences) { - // if (!stillApplicable(seq)) { - // continue - // } + for (const node of subSequenceNodesToCheck) { + const _bestMatch = getBestMatch(node); - // const [matchedA, matchedB] = applySequence(seq) + if (!_bestMatch) { + const addition = Change.createAddition(b); + changes.push(addition); + iterB.mark(b.index, DiffType.addition); - // matchedANodes += matchedA - // matchedBNodes += matchedB + continue; + } - // if (iterA.nodes.length === matchedANodes) { - // // Report adds - // } + if (_bestMatch.length > bestCandidate.length) { + bestCandidate = _bestMatch; + } + } + + const move = Change.createMove(bestCandidate); + changes.push(move); + markMatched(move); + continue; + } +} + +function markMatched(change: Change) { + for (const segment of change.segments) { + const { a, b } = getIndexesFromSegment(segment); - // if (iterB.nodes.length === matchedBNodes) { - // // Report dels - // } - // } + for (const index of range(a.start, a.end)) { + _context.iterA.mark(index, change.type); + } + + for (const index of range(b.start, b.end)) { + _context.iterB.mark(index, change.type); + } + } } export let _context: Context; diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index 67f612b..9beaed9 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -5,9 +5,10 @@ import { getTsNodes } from "./compilers"; import { Node } from "./node"; import colorFn from "kleur"; import { NodesTable } from "./types"; +import { DiffType } from "../types"; export class Iterator { - nodes: Node[] + nodes: Node[]; lastNodeVisited!: Node; @@ -16,26 +17,28 @@ export class Iterator { nodesTable: NodesTable; + matchNumber = 0; + constructor(source: string, side: Side) { this.side = side; this.source = source; const { nodesTable, nodes } = getTsNodes(source, side); - this.nodes = nodes + this.nodes = nodes; this.nodesTable = nodesTable; } - // Get the next unmatched node in the iterator, optionally after a given index - next(index?: number) { - const start = index ?? 0 + // Get the next unmatched node in the iterator, optionally after a given index + next(index?: number) { + const start = index ?? 0; for (let i = start; i < this.nodes.length; i++) { const node = this.nodes[i]; if (node.matched) { continue; } - + return this.lastNodeVisited = node; } } @@ -50,6 +53,18 @@ export class Iterator { return item; } + mark(index: number, markedAs: DiffType) { + // TODO: Should only apply for moves, otherwise a move, addition and move + // will display 1 for the first move and 3 for the second + this.matchNumber++; + this.nodes[index].matched = true; + this.nodes[index].matchNumber = this.matchNumber; + this.nodes[index].markedAs = markedAs; + + // TODO2: Maybe we can delete the node from the `nodesTable` so that the `getMatchingNodes` doesn't need to filter out the marked ones + //this.nodesTable.get(this.nodes[index].kind)!.findIndex/() + } + getMatchingNodes(targetNode: Node) { const rawCandidates = this.nodesTable.get(targetNode.kind); @@ -57,7 +72,7 @@ export class Iterator { return []; } - return rawCandidates.filter(x => x.text === targetNode.text); + return rawCandidates.filter((x) => !x.matched && x.text === targetNode.text); } printNode(node: Node) { diff --git a/src/v2/node.ts b/src/v2/node.ts index 565c164..a6dd879 100644 --- a/src/v2/node.ts +++ b/src/v2/node.ts @@ -1,6 +1,6 @@ import { getPrettyKind } from "../debug"; import { SyntaxKind } from "./types"; -import { Range } from "../types"; +import { DiffType, Range } from "../types"; import { Side } from "../shared/language"; import { _context } from "."; interface NewNodeArgs { @@ -16,7 +16,7 @@ interface NewNodeArgs { end: number; parent: Node | undefined; - isTextNode: boolean + isTextNode: boolean; } export class Node { @@ -32,6 +32,9 @@ export class Node { matched = false; prettyKind: string; + matchNumber = 0; + // For printing proposes + markedAs?: DiffType; constructor(args: NewNodeArgs) { const { side, index, globalIndex, kind, text, parent, start, end, isTextNode } = args; @@ -44,7 +47,7 @@ export class Node { this.start = start; this.end = end; this.parent = parent; - this.isTextNode = isTextNode + this.isTextNode = isTextNode; this.prettyKind = getPrettyKind(kind); } diff --git a/src/v2/types.ts b/src/v2/types.ts index fe6b5bd..80e56b7 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -6,24 +6,7 @@ export type SyntaxKind = number; export type NodesTable = Map; export interface ParsedProgram { - nodes: Node[] - allNodes: Node[] + nodes: Node[]; + allNodes: Node[]; nodesTable: NodesTable; } - -// Start is inclusive, end is not inclusive -export type SegmentRange = [startIndex: number, endIndex: number] - -export interface Segment { - type: DiffType; - a: SegmentRange; - b: SegmentRange; -} - -// TODO: Use a better way to store this -export interface Sequence { - starterNode: Node; - length: number; - skips: number; - segments: Segment[] -} \ No newline at end of file diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 096a719..ac84940 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -1,4 +1,10 @@ +import { _context } from "."; +import { assert } from "../debug"; +import { Side } from "../shared/language"; +import { range } from "../utils"; +import { CandidateMatch, Change, Segment } from "./diff"; import { Iterator } from "./iterator"; +import { Node } from "./node"; export class Context { // Iterators will get stored once they are initialize, which happens later on the execution @@ -9,4 +15,43 @@ export class Context { public sourceA: string, public sourceB: string, ) {} -} \ No newline at end of file +} + +export function getAllNodesFromMatch(match: CandidateMatch, side = Side.b) { + const iter = getIterFromSide(side); + const readFrom = side === Side.a ? 0 : 1; + + const nodes: Node[] = []; + for (const segment of match.segments) { + const { start, end } = getIndexesFromSegment(segment)[side]; + + for (const index of range(start, end)) { + const node = iter.nodes[index]; + + assert(node); + + nodes.push(node); + } + } + + return nodes; +} + +export function getIterFromSide(side: Side): Iterator { + return side === Side.a ? _context.iterA : _context.iterB; +} + +export function getIndexesFromSegment(segment: Segment) { + const [startA, startB, length] = segment; + + return { + a: { + start: startA, + end: startA + length, + }, + b: { + start: startB, + end: startB + length, + }, + }; +} diff --git a/tests/conformance/additions-removals.test.ts b/tests/conformance/additions-removals.test.ts index 0698bf4..f78f713 100644 --- a/tests/conformance/additions-removals.test.ts +++ b/tests/conformance/additions-removals.test.ts @@ -14,8 +14,8 @@ describe("Properly report lines added and removed", () => { expB: ` let name; ➕let age;➕ - ` - }) + `, + }); test({ name: "Single line added above", @@ -32,8 +32,8 @@ describe("Properly report lines added and removed", () => { expB: ` ➕let age;➕ let name; - ` - }) + `, + }); test({ name: "Multiple lines added 1", @@ -44,8 +44,8 @@ describe("Properly report lines added and removed", () => { expB: ` ➕let a; let b;➕ - ` - }) + `, + }); test({ name: "Multiple lines added 2", @@ -58,8 +58,8 @@ describe("Properly report lines added and removed", () => { ➕let a; let b; let c;➕ - ` - }) + `, + }); test({ name: "Multiple lines added 3. With trivia", @@ -68,8 +68,8 @@ describe("Properly report lines added and removed", () => { `, expB: `➕let a; let b;➕ - ` - }) + `, + }); test({ name: "Added wrapped code", @@ -88,8 +88,8 @@ describe("Properly report lines added and removed", () => { ➕while (true) {➕ callFn() ➕}➕ - ` - }) + `, + }); // This used to crash test({ @@ -105,6 +105,6 @@ describe("Properly report lines added and removed", () => { `, expB: ` ➕() {}➕ - ` - }) + `, + }); }); diff --git a/tests/conformance/alignment.test.ts b/tests/conformance/alignment.test.ts index 7e8b199..ab01340 100644 --- a/tests/conformance/alignment.test.ts +++ b/tests/conformance/alignment.test.ts @@ -1,13 +1,13 @@ import { describe } from "vitest"; import { getTestFn } from "../utils"; -import { OutputType, getDiff } from "../../src"; +import { getDiff, OutputType } from "../../src"; import colorFn from "kleur"; -const test = getTestFn(getDiff, { outputType: OutputType.alignedText, alignmentText: colorFn.cyan("<>"), ignoreChangeMarkers: true }) +const test = getTestFn(getDiff, { outputType: OutputType.alignedText, alignmentText: colorFn.cyan("<>"), ignoreChangeMarkers: true }); describe.skip("Properly align code", () => { test({ - name: 'Basic case 1', + name: "Basic case 1", a: ` 1 2 @@ -25,11 +25,11 @@ describe.skip("Properly align code", () => { 1 <> <> - ` - }) + `, + }); test({ - name: 'Basic case 2', + name: "Basic case 2", a: ` 1 2 @@ -48,11 +48,11 @@ describe.skip("Properly align code", () => { 1 2 <> - ` - }) + `, + }); test({ - name: 'Basic case 3', + name: "Basic case 3", a: ` 1 2 @@ -71,11 +71,11 @@ describe.skip("Properly align code", () => { 1 <> 3 - ` - }) + `, + }); test({ - name: 'Basic case 4', + name: "Basic case 4", a: ` 1 2 @@ -93,11 +93,11 @@ describe.skip("Properly align code", () => { <> <> 3 - ` - }) + `, + }); test({ - name: 'Basic case 5', + name: "Basic case 5", a: ` 1 2 @@ -115,11 +115,11 @@ describe.skip("Properly align code", () => { <> <> 3 - ` - }) + `, + }); test({ - name: 'Basic case 6', + name: "Basic case 6", a: ` 123 A @@ -137,12 +137,12 @@ describe.skip("Properly align code", () => { B 123 <> - ` - }) + `, + }); test({ - only: 'standard', - name: 'Basic case 7', + only: "standard", + name: "Basic case 7", a: ` A 123 @@ -151,10 +151,10 @@ describe.skip("Properly align code", () => { B 123 `, - }) + }); test({ - name: 'Basic case 8', + name: "Basic case 8", a: ` x 123 @@ -172,11 +172,11 @@ describe.skip("Properly align code", () => { <> 123 x - ` - }) + `, + }); test({ - name: 'Basic case 9', + name: "Basic case 9", disabled: true, a: ` 123 @@ -196,11 +196,11 @@ describe.skip("Properly align code", () => { x 123 z - ` - }) + `, + }); test({ - name: 'Basic case 10', + name: "Basic case 10", a: ` 123 x @@ -225,11 +225,11 @@ describe.skip("Properly align code", () => { z <> 5 - ` - }) + `, + }); test({ - name: 'Basic case 11', + name: "Basic case 11", a: ` x y 123 @@ -247,11 +247,11 @@ describe.skip("Properly align code", () => { <> 123 x - ` - }) + `, + }); test({ - name: 'Basic case 12', + name: "Basic case 12", a: ` console.log() `, @@ -272,17 +272,17 @@ describe.skip("Properly align code", () => { 2 3 console.log() - ` - }) + `, + }); // Test insertion into "lastA" and "lastB" test({ - name: 'Basic case 13', + name: "Basic case 13", a: "xx\n1", b: "1\nxx", expA: "<>\nxx\n1", - expB: "1\nxx\n<>" - }) + expB: "1\nxx\n<>", + }); test({ name: "Basic case 13b", @@ -303,13 +303,13 @@ describe.skip("Properly align code", () => { 1 xx <> - ` - }) + `, + }); // Ignored backward because it's order dependant test({ - name: 'Basic case 14', - only: 'standard', + name: "Basic case 14", + only: "standard", a: ` 1 2 @@ -335,12 +335,12 @@ describe.skip("Properly align code", () => { 5 console .log() - ` - }) + `, + }); // TODO(Alignment): Take the most wight in order to specify where to put the alignment, either at the beginning or the end test({ - name: 'Basic case 15', + name: "Basic case 15", a: ` x if (true) {} @@ -362,11 +362,11 @@ describe.skip("Properly align code", () => { if (true) {} z `, - }) + }); test({ - name: 'Basic case 16', - only: 'standard', + name: "Basic case 16", + only: "standard", a: ` zzz 1 @@ -399,11 +399,11 @@ describe.skip("Properly align code", () => { 3 4 `, - }) + }); // TODO(Alignment): Take the most wight in order to specify where to put the alignment, either at the beginning or the end test({ - name: 'Basic case 17', + name: "Basic case 17", a: ` 1 print(true) {} @@ -429,11 +429,11 @@ describe.skip("Properly align code", () => { true ) { - }` + }`, }); test({ - name: 'Basic case 18', + name: "Basic case 18", a: ` x z @@ -456,10 +456,10 @@ describe.skip("Properly align code", () => { 2 z `, - }) + }); test({ - name: 'Basic case 19', + name: "Basic case 19", a: ` x z 1 2 @@ -478,11 +478,11 @@ describe.skip("Properly align code", () => { x <> 3 - ` - }) + `, + }); test({ - name: 'Basic case 20', + name: "Basic case 20", a: ` x console.log(0) @@ -502,17 +502,17 @@ describe.skip("Properly align code", () => { console.log(1) x z - ` - }) + `, + }); test({ - name: 'Basic case 21', + name: "Basic case 21", a: "fn(x)", b: "console.log(fn(1))", - }) + }); test({ - name: 'Basic case 22', + name: "Basic case 22", a: ` { { a, b, x } = obj @@ -535,11 +535,11 @@ describe.skip("Properly align code", () => { { x } = obj z } - ` - }) + `, + }); test({ - name: 'Basic case 23', + name: "Basic case 23", a: ` 1 x `, @@ -554,11 +554,11 @@ describe.skip("Properly align code", () => { expB: ` 2 x - ` - }) + `, + }); test({ - name: 'Basic case 24', + name: "Basic case 24", a: ` 1 { a: 1 @@ -584,11 +584,11 @@ describe.skip("Properly align code", () => { a: 1 } } - ` - }) + `, + }); test({ - name: 'Basic case 25', + name: "Basic case 25", a: ` 1 1 @@ -638,13 +638,13 @@ describe.skip("Properly align code", () => { x z zz - ` - }) -}) + `, + }); +}); -describe.skip('Properly ignore alignments', () => { +describe.skip("Properly ignore alignments", () => { test({ - name: 'Ignore alignment 1', + name: "Ignore alignment 1", a: ` 1 2 `, @@ -654,10 +654,10 @@ describe.skip('Properly ignore alignments', () => { expA: ` 1 2 `, - }) + }); test({ - name: 'Ignore alignment 2', + name: "Ignore alignment 2", a: ` x 1 2 @@ -672,6 +672,6 @@ describe.skip('Properly ignore alignments', () => { expB: ` <> 1 - ` - }) -}) + `, + }); +}); diff --git a/tests/conformance/changes.test.ts b/tests/conformance/changes.test.ts index 657513d..4f408a2 100644 --- a/tests/conformance/changes.test.ts +++ b/tests/conformance/changes.test.ts @@ -15,8 +15,8 @@ describe("Properly report line changed", () => { `, expB: ` ➕1➕ - ` - }) + `, + }); test({ name: "Single line change 2", @@ -31,8 +31,8 @@ describe("Properly report line changed", () => { `, expB: ` ➕false➕ - ` - }) + `, + }); test({ name: "Single line change 3", @@ -47,8 +47,8 @@ describe("Properly report line changed", () => { `, expB: ` ➕let firstName➕ = "elian" - ` - }) + `, + }); test({ name: "Single line change 4", @@ -63,8 +63,8 @@ describe("Properly report line changed", () => { `, expB: ` let name = ➕"eliam"➕ - ` - }) + `, + }); test({ name: "Single line change 5", @@ -79,8 +79,8 @@ describe("Properly report line changed", () => { `, expB: ` console.log(➕1➕) - ` - }) + `, + }); // TODO: This test got downgraded with the inclusion of the single node matching policy, if we introduce an LCS skip count nodes we may regain the old output test({ @@ -96,6 +96,6 @@ describe("Properly report line changed", () => { `, expB: ` ➕let firstName = "eliam"➕ - ` - }) + `, + }); }); diff --git a/tests/conformance/move.test.ts b/tests/conformance/move.test.ts index 646637e..1f66004 100644 --- a/tests/conformance/move.test.ts +++ b/tests/conformance/move.test.ts @@ -19,8 +19,8 @@ describe("Properly report lines added", () => { expB: ` ⏩b⏪ aa - ` - }) + `, + }); test({ name: "Simple move 2", @@ -39,8 +39,8 @@ describe("Properly report lines added", () => { expB: ` ⏩1⏪ 1 2 - ` - }) + `, + }); test({ name: "Multi characters move 2", @@ -63,8 +63,8 @@ describe("Properly report lines added", () => { ⏩let age;⏪ console.log() let name = 'Elian' - ` - }) + `, + }); test({ name: "LCS case 1", @@ -93,8 +93,8 @@ describe("Properly report lines added", () => { 1 2 3 - ` - }) + `, + }); test({ name: "LCS case 2", @@ -125,8 +125,8 @@ describe("Properly report lines added", () => { ➕1 2➕ 3 - ` - }) + `, + }); test({ name: "Mix of move with deletions and additions", @@ -141,8 +141,8 @@ describe("Properly report lines added", () => { `, expB: ` ➕fn(➕console.log(➕2➕)➕)➕ - ` - }) + `, + }); test({ name: "Mix of move with deletions and additions 2", @@ -157,8 +157,8 @@ describe("Properly report lines added", () => { `, expB: ` ➕console.log(➕fn(➕1➕)➕)➕ - ` - }) + `, + }); test({ name: "Mix of move with deletions and additions 3", @@ -173,8 +173,8 @@ describe("Properly report lines added", () => { `, expB: ` ➕fn(➕console.log(➕2➕)➕)➕ - ` - }) + `, + }); test({ name: "Properly match closing paren", @@ -189,8 +189,8 @@ describe("Properly report lines added", () => { `, expB: ` console.log(➕fn()➕) - ` - }) + `, + }); test({ name: "Properly match closing paren 2", @@ -213,8 +213,8 @@ describe("Properly report lines added", () => { ➕z➕ print(➕123➕) ➕x➕ - ` - }) + `, + }); test({ name: "Properly match closing paren 3", @@ -233,8 +233,8 @@ describe("Properly report lines added", () => { ➕function asd () {➕ console.log(➕"hi"➕) ➕}➕ - ` - }) + `, + }); test({ name: "Properly match closing paren 4", @@ -255,8 +255,8 @@ describe("Properly report lines added", () => { `, expB: ` print(➕123➕) - ` - }) + `, + }); test({ name: "Properly match closing paren 5", @@ -271,8 +271,8 @@ describe("Properly report lines added", () => { `, expB: ` ➕console.log(➕fn(➕1➕)➕)➕ - ` - }) + `, + }); test({ name: "Properly match closing paren 6", @@ -309,8 +309,8 @@ describe("Properly report lines added", () => { a: 1 } ➕}➕ - ` - }) + `, + }); // Test closing the paren on a deletion / addition on the "verifySingle" test({ @@ -334,8 +334,8 @@ describe("Properly report lines added", () => { `, expB: ` ➕console.log(➕1➕)➕ - ` - }) + `, + }); // Test closing the paren on a move with syntax error test({ @@ -365,8 +365,8 @@ describe("Properly report lines added", () => { 123 123 ➕Z➕ - ` - }) + `, + }); // Test for another syntax error, this hit the branch where we initialize a stack with a closing paren test({ @@ -382,8 +382,8 @@ describe("Properly report lines added", () => { `, expB: ` ➕}{➕ - ` - }) + `, + }); // Test the ignore matches in the process moves test({ @@ -409,8 +409,8 @@ describe("Properly report lines added", () => { { x } = obj ➕z➕ } - ` - }) + `, + }); // Also test the match ignoring logic, now inside the true branch on the alignment @@ -439,8 +439,8 @@ describe("Properly report lines added", () => { { (➕c➕) } - ` - }) + `, + }); // Testing single node matching test({ @@ -476,8 +476,8 @@ describe("Properly report lines added", () => { expB: ` ➕const➕ var1 = foo() ➕const➕ var2 = bar() - ` - }) + `, + }); // This used to break the inverse test({ @@ -493,6 +493,6 @@ describe("Properly report lines added", () => { `, expB: ` ➕(➕1➕)➕ - ` - }) + `, + }); }); diff --git a/tests/conformance/sequence-matching.test.ts b/tests/conformance/sequence-matching.test.ts index 8490bc3..391235b 100644 --- a/tests/conformance/sequence-matching.test.ts +++ b/tests/conformance/sequence-matching.test.ts @@ -17,8 +17,8 @@ describe("Properly report moves in a same sequence", () => { expB: ` print('elian') ⏩let age = 24⏪ - ` - }) + `, + }); test({ name: "Case 2", @@ -35,8 +35,8 @@ describe("Properly report moves in a same sequence", () => { expB: ` let age = 24 print('elian') - ` - }) + `, + }); test({ name: "Case 3", @@ -53,8 +53,8 @@ describe("Properly report moves in a same sequence", () => { expB: ` let age = ⏩24⏪ print('elian') - ` - }) + `, + }); test({ name: "Case 4", @@ -71,8 +71,8 @@ describe("Properly report moves in a same sequence", () => { expB: ` print('elian') ⏩let age =⏪ 24 - ` - }) + `, + }); test({ name: "Back and forth", @@ -93,8 +93,8 @@ describe("Properly report moves in a same sequence", () => { expB: ` let age = 24 ➕||➕ ⏩fn()⏪ print('elian') - ` - }) + `, + }); // TODO: Can be improved test({ @@ -114,8 +114,8 @@ describe("Properly report moves in a same sequence", () => { expB: ` let middle; ➕let down;➕ - ` - }) + `, + }); }); describe("Recursive matching", () => { @@ -136,8 +136,8 @@ describe("Recursive matching", () => { expB: ` ➕1➕ import { bar } from "bar"; - ` - }) + `, + }); test({ name: "Recursive matching 2", @@ -164,8 +164,8 @@ describe("Recursive matching", () => { 0 0➕ 1 2 3 4 - ` - }) + `, + }); test({ name: "Recursive matching 3", @@ -194,8 +194,8 @@ describe("Recursive matching", () => { ➕0 0➕ ⏩12 34⏪ - ` - }) + `, + }); test({ name: "Recursive matching 4", @@ -224,8 +224,8 @@ describe("Recursive matching", () => { ➕0 0➕ ⏩12 34⏪ - ` - }) + `, + }); // This tests going backward in the LCS calculation test({ @@ -265,8 +265,8 @@ describe("Recursive matching", () => { start: range.start }; } - ` - }) + `, + }); // This tests the subsequence matching test({ @@ -288,9 +288,8 @@ describe("Recursive matching", () => { expB: ` 1 import { X } from "./x"; - ` - }) - + `, + }); test({ name: "Random 1", @@ -311,14 +310,14 @@ describe("Recursive matching", () => { expB: ` 33 ⏩2⏪ - ` - }) + `, + }); // The bug that we are testing here is if we have 2 moves, crossing each other and both are of the same length. The result // is that depending of which one gets processed first, that will be aligned, this means that the result is not the same A to B and B to A, // this is why I had to create the cases separated test({ - only: 'standard', + only: "standard", name: "Random 2 standard", a: ` 1 @@ -341,11 +340,11 @@ describe("Recursive matching", () => { ➕5➕ 4 ⏩3⏪ - ` - }) + `, + }); test({ - only: 'inversed', + only: "inversed", name: "Random 2 inversed", a: ` 1 @@ -368,8 +367,8 @@ describe("Recursive matching", () => { ➕5➕ ⏩4⏪ 3 - ` - }) + `, + }); // Used to crash when inserting new line alignments in "processMoves" when aligning two moves test({ @@ -391,6 +390,6 @@ describe("Recursive matching", () => { 22 ➕x➕ ⏩1⏪ - ` - }) -}) \ No newline at end of file + `, + }); +}); diff --git a/tests/conformance/trivia-format.test.ts b/tests/conformance/trivia-format.test.ts index 247938b..7e4b353 100644 --- a/tests/conformance/trivia-format.test.ts +++ b/tests/conformance/trivia-format.test.ts @@ -1,12 +1,12 @@ import { describe } from "vitest"; import { getTestFn } from "../utils"; -import { OutputType, getDiff } from "../../src"; +import { getDiff, OutputType } from "../../src"; -const test = getTestFn(getDiff, { outputType: OutputType.alignedText, alignmentText: " <>", ignoreChangeMarkers: true }) +const test = getTestFn(getDiff, { outputType: OutputType.alignedText, alignmentText: " <>", ignoreChangeMarkers: true }); describe.skip("Properly align formatted code", () => { test({ - name: 'Case 1', + name: "Case 1", a: ` x `, @@ -17,11 +17,11 @@ describe.skip("Properly align formatted code", () => { expA: ` <> x - ` - }) + `, + }); test({ - name: 'Case 2', + name: "Case 2", a: ` x `, @@ -34,11 +34,11 @@ describe.skip("Properly align formatted code", () => { <> <> x - ` - }) + `, + }); test({ - name: 'Case 3', + name: "Case 3", a: ` print() @@ -55,9 +55,8 @@ describe.skip("Properly align formatted code", () => { `, }); - test({ - name: 'Case 4', + name: "Case 4", a: ` 1 print() @@ -76,7 +75,7 @@ describe.skip("Properly align formatted code", () => { // TODO(Improve): If a whole line is deleted / added and to the other side we have a matching alignment, we could skip both. The bellow example can be compacted test({ - name: 'Case 5', + name: "Case 5", a: ` 1 print() @@ -99,7 +98,7 @@ describe.skip("Properly align formatted code", () => { // TODO(Improve): Another example of compaction as mentioned above test({ - name: 'Case 6', + name: "Case 6", a: ` 1 x @@ -121,12 +120,11 @@ describe.skip("Properly align formatted code", () => { <> 2 - ` + `, }); - test({ - name: 'Case 7', + name: "Case 7", a: ` 1 print() @@ -147,7 +145,7 @@ describe.skip("Properly align formatted code", () => { }); test({ - name: 'Case 8', + name: "Case 8", a: ` 1 2 @@ -172,7 +170,7 @@ describe.skip("Properly align formatted code", () => { }); test({ - name: 'Case 9', + name: "Case 9", a: ` 1x `, @@ -181,12 +179,12 @@ describe.skip("Properly align formatted code", () => { `, expA: ` 1x - ` + `, }); // TODO(Alignment): Take the most wight in order to specify where to put the alignment, either at the beginning or the end test({ - name: 'Case 10', + name: "Case 10", a: ` print() `, @@ -201,7 +199,7 @@ describe.skip("Properly align formatted code", () => { }); test({ - name: 'Case 11', + name: "Case 11", a: ` print() `, @@ -219,9 +217,8 @@ describe.skip("Properly align formatted code", () => { `, }); - test({ - name: 'Case 12', + name: "Case 12", a: ` console.log() `, @@ -248,7 +245,7 @@ describe.skip("Properly align formatted code", () => { console . log() - ` + `, }); test({ @@ -279,8 +276,8 @@ describe.skip("Properly align formatted code", () => { <> <> <> - ` - }) + `, + }); test({ name: "Case 14", @@ -297,7 +294,7 @@ describe.skip("Properly align formatted code", () => { <> () `, - }) + }); test({ name: "Case 15", @@ -318,10 +315,10 @@ describe.skip("Properly align formatted code", () => { <> ) `, - }) + }); test({ - name: 'Case 16', + name: "Case 16", a: ` console.log() `, @@ -336,11 +333,11 @@ describe.skip("Properly align formatted code", () => { expB: ` console.log( ) - ` - }) + `, + }); test({ - name: 'Case 17', + name: "Case 17", a: ` console.log() `, @@ -355,11 +352,11 @@ describe.skip("Properly align formatted code", () => { <> <> console.log() - ` - }) + `, + }); test({ - name: 'Case 18', + name: "Case 18", a: ` {} `, @@ -372,11 +369,11 @@ describe.skip("Properly align formatted code", () => { <> <> {} - ` - }) + `, + }); test({ - name: 'Case 19', + name: "Case 19", a: ` () x @@ -392,8 +389,8 @@ describe.skip("Properly align formatted code", () => { <> () x - ` - }) + `, + }); test({ name: "Case 20", @@ -424,11 +421,11 @@ describe.skip("Properly align formatted code", () => { ) end - ` - }) + `, + }); test({ - name: 'Case 21', + name: "Case 21", a: ` () x @@ -470,11 +467,11 @@ describe.skip("Properly align formatted code", () => { ) x zz - ` - }) + `, + }); test({ - name: 'Case 22', + name: "Case 22", a: ` 1 2 x @@ -485,22 +482,22 @@ describe.skip("Properly align formatted code", () => { expB: ` <> 1 2 x - ` - }) + `, + }); // TODO: Another example of compression test({ - name: 'Case 23', + name: "Case 23", a: ` () `, b: ` (x) `, - }) + }); test({ - name: 'Case 24', + name: "Case 24", a: ` 1 2 3 `, @@ -514,11 +511,11 @@ describe.skip("Properly align formatted code", () => { <> 1 2 3 `, - }) + }); // Testing the ignoring the push down of alignments test({ - name: 'Case 25', + name: "Case 25", a: ` x z 1 `, @@ -536,8 +533,8 @@ describe.skip("Properly align formatted code", () => { 1 x z - ` - }) + `, + }); test({ name: "Case 26", @@ -564,11 +561,11 @@ describe.skip("Properly align formatted code", () => { 2 3 <> - ` - }) + `, + }); test({ - name: 'Case 27', + name: "Case 27", a: ` console.log() 1 @@ -581,5 +578,5 @@ describe.skip("Properly align formatted code", () => { console.log() 1 `, - }) -}) + }); +}); diff --git a/tests/lcs.test.ts b/tests/lcs.test.ts index 001884c..cee83c9 100644 --- a/tests/lcs.test.ts +++ b/tests/lcs.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, vi } from "vitest"; import { getSequenceSingleDirection } from "../src/core/find_diffs"; class MockIterator { - constructor(public items: any[]) { } + constructor(public items: any[]) {} peek(index: number) { return this.items[index]; diff --git a/tests/realWorld/case1/a.ts b/tests/realWorld/case1/a.ts index 34e26e9..438ed11 100644 --- a/tests/realWorld/case1/a.ts +++ b/tests/realWorld/case1/a.ts @@ -12,7 +12,7 @@ export class Change { public rangeB: Range | undefined, public nodeA: Node | undefined, public nodeB: Node | undefined, - ) { } + ) {} draw() { const { sourceA, sourceB } = getContext(); diff --git a/tests/realWorld/case1/b.ts b/tests/realWorld/case1/b.ts index 71a5e41..382761b 100644 --- a/tests/realWorld/case1/b.ts +++ b/tests/realWorld/case1/b.ts @@ -1,4 +1,4 @@ -1 +1; import { colorFn, getSourceWithChange } from "./reporter"; import { Node } from "./node"; import { getContext } from "./index"; @@ -12,7 +12,7 @@ export class Change { public rangeB: Range | undefined, public nodeA: Node | undefined, public nodeB: Node | undefined, - ) { } + ) {} draw() { const { sourceA, sourceB } = getContext(); diff --git a/tests/realWorld/case2/a.ts b/tests/realWorld/case2/a.ts index 6165990..9330032 100644 --- a/tests/realWorld/case2/a.ts +++ b/tests/realWorld/case2/a.ts @@ -1,3 +1,3 @@ import ts from "typescript"; import { Node } from "./node"; -import { Range, Side } from "./types"; \ No newline at end of file +import { Range, Side } from "./types"; diff --git a/tests/realWorld/case3/a.ts b/tests/realWorld/case3/a.ts index e91772b..dab33bf 100644 --- a/tests/realWorld/case3/a.ts +++ b/tests/realWorld/case3/a.ts @@ -6,4 +6,4 @@ function isNotOverload(declaration: Declaration): boolean { /** @internal */ export function signatureHasLiteralTypes(s: Signature) { return !!(s.flags & SignatureFlags.HasLiteralTypes); -} \ No newline at end of file +} diff --git a/tests/realWorld/realWordRunner.test.ts b/tests/realWorld/realWordRunner.test.ts index 43b62e0..45b7049 100644 --- a/tests/realWorld/realWordRunner.test.ts +++ b/tests/realWorld/realWordRunner.test.ts @@ -1,54 +1,54 @@ -import { readdirSync, promises } from 'node:fs' -import { join } from 'node:path' -import { describe, expect, test } from 'vitest' -import { getDiff } from '../../src' +import { promises, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { getDiff } from "../../src"; // TODO: Fix -const IGNORED_TESTS: string[] = [] +const IGNORED_TESTS: string[] = []; -const path = join(__dirname, './') -let dirs = readdirSync(path) +const path = join(__dirname, "./"); +let dirs = readdirSync(path); // Remove the test runner file which is always the last one -dirs.splice(dirs.length - 1, 1) +dirs.splice(dirs.length - 1, 1); -describe('Real world tests', async () => { +describe("Real world tests", async () => { for (const testCase of dirs) { if (IGNORED_TESTS.includes(testCase)) { - continue + continue; } - const { a, b } = await getTestFilesFromDir(testCase) + const { a, b } = await getTestFilesFromDir(testCase); test(testCase, () => { try { - getDiff(a, b) + getDiff(a, b); } catch (error) { - expect((error as any)?.message).toBe(undefined) + expect((error as any)?.message).toBe(undefined); } - }) + }); test(`${testCase} inverse`, async () => { try { - getDiff(b, a) + getDiff(b, a); } catch (error) { - expect((error as any)?.message).toBe(undefined) + expect((error as any)?.message).toBe(undefined); } - }) + }); } -}) +}); async function getTestFilesFromDir(dir: string) { - const pathA = join(path, dir, 'a.ts') - const pathB = join(path, dir, 'b.ts') + const pathA = join(path, dir, "a.ts"); + const pathB = join(path, dir, "b.ts"); const [a, b] = await Promise.all([ promises.readFile(pathA), promises.readFile(pathB), - ]) + ]); return { a: a.toString(), - b: b.toString() - } -} \ No newline at end of file + b: b.toString(), + }; +} diff --git a/tests/unitTests/serializer.test.ts b/tests/unitTests/serializer.test.ts index 02f580c..69ec65d 100644 --- a/tests/unitTests/serializer.test.ts +++ b/tests/unitTests/serializer.test.ts @@ -1,19 +1,18 @@ import { expect, test } from "vitest"; -import { OutputType, getDiff } from "../../src"; +import { getDiff, OutputType } from "../../src"; -test('Case 1', () => { +test("Case 1", () => { const a = ` console.log() && 3 - `.trim() + `.trim(); const b = ` fn(console.log(1)) - `.trim() + `.trim(); const expected = { chunksA: [ - [ - ], + [], [ { text: "console.log() ", @@ -29,8 +28,7 @@ test('Case 1', () => { ], chunksB: [ - [ - ], + [], [ { text: "fn(", @@ -58,26 +56,26 @@ test('Case 1', () => { moveNumber: "", }, ], - ] - } + ], + }; - const result = getDiff(a, b, { outputType: OutputType.serializedChunks }) + const result = getDiff(a, b, { outputType: OutputType.serializedChunks }); - expect(result).toEqual(expected) -}) + expect(result).toEqual(expected); +}); -test('Case 2', () => { +test("Case 2", () => { const a = ` if (true) { 3; print } - `.trim() + `.trim(); const b = ` z print; 3 x - `.trim() + `.trim(); const expected = { chunksA: [ @@ -181,9 +179,9 @@ test('Case 2', () => { }, ], ], - } + }; - const result = getDiff(a, b, { outputType: OutputType.serializedChunks }) + const result = getDiff(a, b, { outputType: OutputType.serializedChunks }); - expect(result).toEqual(expected) -}) \ No newline at end of file + expect(result).toEqual(expected); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 929f8cd..589a90b 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,16 +1,16 @@ import { expect, test as vTest } from "vitest"; -import { Options, OutputType, getDiff } from "../src"; +import { getDiff, Options, OutputType } from "../src"; // Re-use type type TestFn = (...args: any[]) => { sourceA: string; sourceB: string; -} +}; interface TestInfo { disabled?: boolean; - only?: 'standard' | 'inversed', - name?: string | number, + only?: "standard" | "inversed"; + name?: string | number; a?: string; b?: string; expA?: string; @@ -18,56 +18,56 @@ interface TestInfo { } export function getTestFn(testFn: TestFn, testOptions: Options = {}) { - const options = { outputType: OutputType.text, ...testOptions } + const options = { outputType: OutputType.text, ...testOptions }; return function test(testInfo: TestInfo) { - const { a = '', b = '', expA, expB, name = "anonymous" } = testInfo; + const { a = "", b = "", expA, expB, name = "anonymous" } = testInfo; if (a === expA && b === expB) { - throw new Error(`Invalid test ${name}, input and output are the same`) + throw new Error(`Invalid test ${name}, input and output are the same`); } if (testInfo.disabled) { - return + return; } - const skipStandardTest = testInfo.only === 'inversed'; + const skipStandardTest = testInfo.only === "inversed"; if (!skipStandardTest) { vTest(`Test ${name}`, () => { - const { sourceA: resultA, sourceB: resultB } = testFn(a, b, options) + const { sourceA: resultA, sourceB: resultB } = testFn(a, b, options); validateDiff(expA || a, expB || b, resultA, resultB); }); } - const skipInversedTest = testInfo.only === 'standard'; + const skipInversedTest = testInfo.only === "standard"; if (!skipInversedTest) { vTest(`Test ${name} inverse`, () => { - const { sourceA: resultA, sourceB: resultB } = testFn(b, a, options) + const { sourceA: resultA, sourceB: resultB } = testFn(b, a, options); const inversedExpectedA = getInversedExpectedResult(expB || b); - const inversedExpectedB = getInversedExpectedResult(expA || a) + const inversedExpectedB = getInversedExpectedResult(expA || a); validateDiff(inversedExpectedA, inversedExpectedB, resultA, resultB); }); } - } + }; } export function getInversedExpectedResult(expected: string) { - return expected.split('').map(char => { + return expected.split("").map((char) => { if (char === "➖") { - return "➕" + return "➕"; } else if (char === "➕") { - return "➖" + return "➖"; } else { - return char + return char; } - }).join('') + }).join(""); } -export const test = getTestFn(getDiff) +export const test = getTestFn(getDiff); export function validateDiff( expectedA: string, diff --git a/tsconfig.json b/tsconfig.json index 3f3b73b..1e54182 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -97,10 +97,11 @@ "./tests", "./scripts", "./bench", - "./examples" + "internal/examples" ], "exclude": [ "./ui", - "./tests/realWorld" + "./tests/realWorld", + "./algos/*" ] } From 01a64746f8a21191b340114a0df6cee6ea358167 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 1 Mar 2024 18:49:53 -0300 Subject: [PATCH 12/79] Workinggggg --- src/debug.ts | 79 --------------- src/v2/core.ts | 52 ++++++---- src/v2/diff.ts | 35 +++++-- src/v2/index.ts | 12 ++- src/v2/printer.ts | 246 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 311 insertions(+), 113 deletions(-) create mode 100644 src/v2/printer.ts diff --git a/src/debug.ts b/src/debug.ts index 691eb41..ae0b290 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -147,82 +147,3 @@ export function getNodeForPrinting(kind: number, text: string | undefined) { text: _text, }; } - -// V2 -const COLOR_ROULETTE = [ - colorFn.red, - colorFn.green, - colorFn.yellow, - colorFn.blue, - colorFn.magenta, - colorFn.cyan, -]; - -let _currentColor = -1; -export function getColor() { - _currentColor++; - - if (_currentColor >= COLOR_ROULETTE.length) { - _currentColor = 0; - } - - return COLOR_ROULETTE[_currentColor]; -} - -function resetColorRoulette() { - _currentColor = 0; -} - -function getStringPositionsFromRange( - iter: Iterator, - indexStart: number, - indexEnd: number, -): [start: number, end: number] { - const start = iter.nodes[indexStart].start; - const end = iter.nodes[indexEnd].end; - - return [start, end]; -} - -export function getPrettyStringFromChange( - change: Change, - sourceA: string, - sourceB: string, -) { - const { iterA, iterB } = _context2; - let charsA = sourceA.split(""); - let charsB = sourceB.split(""); - - for (const segment of change.segments) { - const { a, b } = getIndexesFromSegment(segment); - const [startA, endA] = getStringPositionsFromRange(iterA, a.start, a.end); - const [startB, endB] = getStringPositionsFromRange(iterB, b.start, b.end); - - const color = getColor(); - - charsA = getSourceWithChange(charsA, startA, endA, color); - charsB = getSourceWithChange(charsB, startB, endB, color); - } - - return { - a: charsA.join(""), - b: charsB.join(""), - }; -} - -export function prettyPrintChanges(a: string, b: string, changes: Change[]) { - let sequenceCounter = -1; - for (const change of changes) { - sequenceCounter++; - console.log( - `\n---------- Starter ${change.startNode.prettyKind} ${`"${change.startNode.text}"` || ""} Length: ${change.length} Segments ${change.segments.length} Skips: ${change.skips} ----------\n`, - ); - const sourcesWithColor = getPrettyStringFromChange(change, a, b); - - const table = createTextTable(sourcesWithColor.a, sourcesWithColor.b); - - console.log(table); - - resetColorRoulette(); - } -} diff --git a/src/v2/core.ts b/src/v2/core.ts index a4a1c9d..b53c22d 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -1,7 +1,7 @@ import { _context } from "."; import { SyntaxKind } from "./types"; import { Node } from "./node"; -import { CandidateMatch, Change, findSegmentLength, Segment } from "./diff"; +import { CandidateMatch, Change, getCandidateMatch } from "./diff"; import { getAllNodesFromMatch } from "./utils"; import { fail } from "../debug"; import { Iterator } from "./iterator"; @@ -13,8 +13,9 @@ import { DiffType } from "../types"; export function getBestMatch(nodeB: Node): CandidateMatch | undefined { const aSideCandidates = _context.iterA.getMatchingNodes(nodeB); + // The given B node wasn't found, it was added if (aSideCandidates.length === 0) { - fail("WTF2"); + return; } let bestMatch: CandidateMatch = { @@ -24,10 +25,10 @@ export function getBestMatch(nodeB: Node): CandidateMatch | undefined { }; for (const candidate of aSideCandidates) { - const currentMatch = findSegmentLength(candidate, nodeB); + const newCandidate = getCandidateMatch(candidate, nodeB); - if (currentMatch.length > bestMatch.length) { - bestMatch = currentMatch; + if (isNewCandidateBetter(bestMatch, newCandidate)) { + bestMatch = newCandidate; } } @@ -37,26 +38,19 @@ export function getBestMatch(nodeB: Node): CandidateMatch | undefined { export function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { const nodesInMatch = getAllNodesFromMatch(match); - const kinds = new Set(); + const allNodes = new Set(); for (const node of nodesInMatch) { - kinds.add(node.kind); - } - - const allNodes: Node[] = []; - for (const kind of kinds) { - const nodesOfKind = _context.iterB.nodesTable.get(kind); + const similarNodes = _context.iterB.getMatchingNodes(node); - if (!nodesOfKind) { - fail(); + for (const _node of similarNodes) { + allNodes.add(_node); } - allNodes.push(...nodesOfKind); } - const indexOfStartNode = allNodes.findIndex((x) => x.index === starterNode.index); - allNodes.splice(indexOfStartNode, 1); + allNodes.delete(starterNode); - return allNodes; + return [...allNodes]; } export function oneSidedIteration( @@ -66,7 +60,7 @@ export function oneSidedIteration( ): Change[] { const changes: Change[] = []; - let node = iter.next(startFrom); + let node = iter.next(); // TODO: Compactar? while (node) { @@ -78,9 +72,29 @@ export function oneSidedIteration( change = Change.createDeletion(node); } + changes.push(change); iter.mark(node.index, typeOfChange); node = iter.next(node.index + 1); } return changes; } + +/** + * TODO: MAybe compute a score fn + */ +export function isNewCandidateBetter(currentCandidate: CandidateMatch, newCandidate: CandidateMatch): boolean { + if (newCandidate.length > currentCandidate.length) { + return true; + } else if (newCandidate.length < currentCandidate.length) { + return false; + } + + if (newCandidate.segments.length > currentCandidate.segments.length) { + return false; + } else if (newCandidate.segments.length < currentCandidate.segments.length) { + return false; + } + + return newCandidate.skips < currentCandidate.skips; +} diff --git a/src/v2/diff.ts b/src/v2/diff.ts index b083f3a..d88fe7f 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -1,7 +1,7 @@ import { _context } from "."; import { assert } from "../debug"; import { Side } from "../shared/language"; -import { DiffType } from "../types"; +import { DiffType, TypeMasks } from "../types"; import { range } from "../utils"; import { Node } from "./node"; import { getIndexesFromSegment } from "./utils"; @@ -23,9 +23,8 @@ export class Change { public segments: Segment[], public skips = 0, ) { - const { b } = getIndexesFromSegment(segments[0]); - this.startNode = _context.iterA.nodes[b.start]; - this.length = calculateLength(segments); + this.startNode = getStarterNode(type, segments); + this.length = calculateCandidateMatchLength(segments); } static createAddition(node: Node) { @@ -44,7 +43,7 @@ export class Change { static createDeletion(node: Node) { assert(node.side === Side.a); - return new Change(DiffType.addition, [ + return new Change(DiffType.deletion, [ [ // Start A node.index, @@ -64,14 +63,14 @@ export class Change { // SCORE FN PARAMETERS const MAX_NODE_SKIPS = 5; -export function findSegmentLength( +export function getCandidateMatch( nodeA: Node, nodeB: Node, -): { segments: Segment[]; skips: number; length: number } { +): CandidateMatch { const segments: Segment[] = []; const { iterA, iterB } = _context; - let bestSequence = 1; + let bestSequence = 0; let skips = 0; // Where the segment starts @@ -81,6 +80,8 @@ export function findSegmentLength( let indexA = nodeA.index; let indexB = nodeB.index; + assert(equals(nodeA, nodeB), () => "Misaligned matched"); + mainLoop: while (true) { // TODO-2 First iteration already has the nodes const nextA = iterA.next(indexA); @@ -131,6 +132,7 @@ export function findSegmentLength( segmentBStart = indexB; indexA = nextIndexA; skips = numberOfSkips; + bestSequence = 0; continue mainLoop; } } @@ -140,7 +142,7 @@ export function findSegmentLength( } return { - length: bestSequence, + length: calculateCandidateMatchLength(segments), skips, segments, }; @@ -150,7 +152,7 @@ function equals(nodeOne: Node, nodeTwo: Node): boolean { return nodeOne.kind === nodeTwo.kind && nodeOne.text === nodeTwo.text; } -function calculateLength(segments: Segment[]) { +export function calculateCandidateMatchLength(segments: Segment[]) { let sum = 0; for (const segment of segments) { @@ -159,3 +161,16 @@ function calculateLength(segments: Segment[]) { return sum; } + +function getStarterNode(type: DiffType, segments: Segment[]) { + const { a, b } = getIndexesFromSegment(segments[0]); + + const iter = type & TypeMasks.AddOrMove ? _context.iterB : _context.iterA; + const index = type & TypeMasks.AddOrMove ? b.start : a.start; + + const node = iter.nodes[index]; + + assert(node, () => "Failed to get starter node"); + + return node; +} diff --git a/src/v2/index.ts b/src/v2/index.ts index 062c4ba..d0da9ef 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,7 +1,7 @@ import { Context, getIndexesFromSegment } from "./utils"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; -import { getBestMatch, getSubSequenceNodes, oneSidedIteration } from "./core"; +import { getBestMatch, getSubSequenceNodes, isNewCandidateBetter, oneSidedIteration } from "./core"; import { Change, Segment } from "./diff"; import { DiffType } from "../types"; import { fail } from "../debug"; @@ -70,9 +70,9 @@ export function getDiff2(sourceA: string, sourceB: string) { let bestCandidate = bestMatchForB; for (const node of subSequenceNodesToCheck) { - const _bestMatch = getBestMatch(node); + const newCandidate = getBestMatch(node); - if (!_bestMatch) { + if (!newCandidate) { const addition = Change.createAddition(b); changes.push(addition); iterB.mark(b.index, DiffType.addition); @@ -80,8 +80,8 @@ export function getDiff2(sourceA: string, sourceB: string) { continue; } - if (_bestMatch.length > bestCandidate.length) { - bestCandidate = _bestMatch; + if (isNewCandidateBetter(bestCandidate, newCandidate)) { + bestCandidate = newCandidate; } } @@ -90,6 +90,8 @@ export function getDiff2(sourceA: string, sourceB: string) { markMatched(move); continue; } + + return changes; } function markMatched(change: Change) { diff --git a/src/v2/printer.ts b/src/v2/printer.ts new file mode 100644 index 0000000..6e422c7 --- /dev/null +++ b/src/v2/printer.ts @@ -0,0 +1,246 @@ +import { asciiRenderFn, assert, createTextTable, fail, RenderFn } from "../debug"; +import { DiffType } from "../types"; +import { Change } from "./diff"; +import { getIndexesFromSegment } from "./utils"; +import { Iterator } from "./iterator"; +import { _context } from "."; +import colorFn from "kleur"; + +export function applyChangesToSources( + sourceA: string, + sourceB: string, + changes: Change[], + renderFn = asciiRenderFn, +) { + let charsA = sourceA.split(""); + let charsB = sourceB.split(""); + + const { iterA, iterB } = _context; + + for (const change of changes) { + switch (change.type) { + case DiffType.addition: { + assert(change.segments.length === 1); + + const { b } = getIndexesFromSegment(change.segments[0]); + + const { start: startIndex, end: endIndex } = b; + + const [start, end] = getStringPositionsFromRange( + iterB, + startIndex, + endIndex - 1, + ); + + charsB = getSourceWithChange( + charsB, + start, + end, + renderFn[DiffType.addition], + ); + break; + } + + case DiffType.deletion: { + assert(change.segments.length === 1); + + const { a } = getIndexesFromSegment(change.segments[0]); + + const { start: startIndex, end: endIndex } = a; + const [start, end] = getStringPositionsFromRange( + iterA, + startIndex, + endIndex - 1, + ); + + charsA = getSourceWithChange( + charsA, + start, + end, + renderFn[DiffType.deletion], + ); + break; + } + + case DiffType.move: { + for (const segment of change.segments) { + const { a, b } = getIndexesFromSegment(segment); + + const { start: startIndexA, end: endIndexA } = a; + const { start: startIndexB, end: endIndexB } = b; + + const [startA, endA] = getStringPositionsFromRange( + iterA, + startIndexA, + endIndexA - 1, + ); + const [startB, endB] = getStringPositionsFromRange( + iterB, + startIndexB, + endIndexB - 1, + ); + + charsA = getSourceWithChange( + charsA, + startA, + endA, + renderFn[DiffType.move], + ); + charsB = getSourceWithChange( + charsB, + startB, + endB, + renderFn[DiffType.move], + ); + } + break; + } + + default: + fail(`Unhandled type "${change.type}"`); + } + } + + return { sourceA: charsA.join(""), sourceB: charsB.join("") }; +} + +export function getSourceWithChange( + chars: string[], + start: number, + end: number, + colorFn: RenderFn, +) { + // This is to handle cases like EndOfFile + // TODO: Think if this is the best way to handle this, maybe we can just ignore the EOF node altogether or modify it + if (start === end) { + return chars; + } + + const head = chars.slice(0, start); + + let text = chars.slice(start, end).join(""); + + const tail = chars.slice(end, chars.length); + + // We need to add entries to the array in order to keep the positions of the nodes accurately, for example: + // + // chars = ['a', 'b', 'c' ] + // After we merge 'a' with 'b', c is now at index 1, instead of 2 + // + // To calculate the characters to add we take the difference between the end and the start and subtract one, + // this is because we need to count for the character we added + const charsToAdd = end - start - 1; + + const compliment = getComplimentArray(charsToAdd); + + // Since we might display the changes in a table, where we split by newlines, simply coloring the whole segment won't work, for example: + // + // (COLOR_START) a + // b (COLOR END) + // + // When splitted you will get ["(COLOR_START) a", " b (COLOR END)"] where only the first line would be colored, not the second + // So to fix it we wrap all the tokens individually + // + // (COLOR_START) a (COLOR END) + // (COLOR_START) b (COLOR END) + // + // So that when splitted you get ["(COLOR_START) a (COLOR END)", "(COLOR_START) b (COLOR END)"] which produces the desired output + text = text + .split(" ") + .map((x) => colorFn(x)) + .join(" "); + + return [...head, colorFn(text), ...compliment, ...tail]; +} + +export function getComplimentArray( + length: number, + fillInCharacter = "", +): string[] { + assert( + length >= 0, + () => `Length of compliment array invalid. Got ${length}`, + ); + + return new Array(length).fill(fillInCharacter); +} + +function getStringPositionsFromRange( + iter: Iterator, + indexStart: number, + indexEnd: number, +): [start: number, end: number] { + const start = iter.nodes[indexStart].start; + const end = iter.nodes[indexEnd].end; + + return [start, end]; +} + +// V2 +const COLOR_ROULETTE = [ + colorFn.red, + colorFn.green, + colorFn.yellow, + colorFn.blue, + colorFn.magenta, + colorFn.cyan, +]; + +let _currentColor = -1; +export function getColor() { + _currentColor++; + + if (_currentColor >= COLOR_ROULETTE.length) { + _currentColor = 0; + } + + return COLOR_ROULETTE[_currentColor]; +} + +function resetColorRoulette() { + _currentColor = 0; +} + +export function getPrettyStringFromChange( + change: Change, + sourceA: string, + sourceB: string, +) { + const { iterA, iterB } = _context; + let charsA = sourceA.split(""); + let charsB = sourceB.split(""); + + for (const segment of change.segments) { + // switch diff type render add del move separtely + const { a, b } = getIndexesFromSegment(segment); + const [startA, endA] = getStringPositionsFromRange(iterA, a.start, a.end - 1); + const [startB, endB] = getStringPositionsFromRange(iterB, b.start, b.end - 1); + + const color = getColor(); + + charsA = getSourceWithChange(charsA, startA, endA, color); + charsB = getSourceWithChange(charsB, startB, endB, color); + } + + return { + a: charsA.join(""), + b: charsB.join(""), + }; +} + +export function prettyPrintChanges(a: string, b: string, changes: Change[]) { + let sequenceCounter = -1; + for (const change of changes) { + sequenceCounter++; + console.log( + `\n---------- Starter ${change.startNode.prettyKind} ${`"${change.startNode.text}"` || ""} Length: ${change.length} Segments ${change.segments.length} Skips: ${change.skips} ----------\n`, + ); + const sourcesWithColor = getPrettyStringFromChange(change, a, b); + + const table = createTextTable(sourcesWithColor.a, sourcesWithColor.b); + + console.log(table); + + resetColorRoulette(); + } +} From 3b90ef8a57ccaf7cb3ebaa2fbca068ef93505791 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 5 Mar 2024 11:47:15 -0300 Subject: [PATCH 13/79] explore match code first pass --- src/v2/core.ts | 4 +- src/v2/diff.ts | 182 +++++++++++++++++++++++++++++++++++++----------- src/v2/index.ts | 4 +- 3 files changed, 145 insertions(+), 45 deletions(-) diff --git a/src/v2/core.ts b/src/v2/core.ts index b53c22d..66b49c4 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -27,7 +27,7 @@ export function getBestMatch(nodeB: Node): CandidateMatch | undefined { for (const candidate of aSideCandidates) { const newCandidate = getCandidateMatch(candidate, nodeB); - if (isNewCandidateBetter(bestMatch, newCandidate)) { + if (isLatterCandidateBetter(bestMatch, newCandidate)) { bestMatch = newCandidate; } } @@ -83,7 +83,7 @@ export function oneSidedIteration( /** * TODO: MAybe compute a score fn */ -export function isNewCandidateBetter(currentCandidate: CandidateMatch, newCandidate: CandidateMatch): boolean { +export function isLatterCandidateBetter(currentCandidate: CandidateMatch, newCandidate: CandidateMatch): boolean { if (newCandidate.length > currentCandidate.length) { return true; } else if (newCandidate.length < currentCandidate.length) { diff --git a/src/v2/diff.ts b/src/v2/diff.ts index d88fe7f..c0fdbca 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -1,10 +1,12 @@ import { _context } from "."; -import { assert } from "../debug"; +import { assert, fail } from "../debug"; import { Side } from "../shared/language"; +import { Iterator } from "./iterator"; import { DiffType, TypeMasks } from "../types"; import { range } from "../utils"; import { Node } from "./node"; import { getIndexesFromSegment } from "./utils"; +import { getBestMatch, isLatterCandidateBetter } from "./core"; // Start is inclusive, end is not inclusive export type Segment = [indexA: number, indexB: number, length: number]; @@ -15,13 +17,21 @@ export interface CandidateMatch { segments: Segment[]; } +export function getEmptyCandidate(): CandidateMatch { + return { + length: 0, + segments: [], + skips: 0, + }; +} + export class Change { startNode: Node; length: number; constructor( public type: DiffType, public segments: Segment[], - public skips = 0, + public skips = 0 ) { this.startNode = getStarterNode(type, segments); this.length = calculateCandidateMatchLength(segments); @@ -63,19 +73,15 @@ export class Change { // SCORE FN PARAMETERS const MAX_NODE_SKIPS = 5; -export function getCandidateMatch( - nodeA: Node, - nodeB: Node, -): CandidateMatch { +export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { const segments: Segment[] = []; const { iterA, iterB } = _context; - let bestSequence = 0; + let segmentLength = 0; let skips = 0; - // Where the segment starts - let segmentAStart = nodeA.index; - let segmentBStart = nodeB.index; + let currentASegmentStart = nodeA.index; + let currentBSegmentStart = nodeB.index; let indexA = nodeA.index; let indexB = nodeB.index; @@ -89,56 +95,102 @@ export function getCandidateMatch( // If one of the iterators ends then there is no more search to do if (!nextA || !nextB) { - segments.push([segmentAStart, segmentBStart, bestSequence]); + segments.push([ + currentASegmentStart, + currentBSegmentStart, + segmentLength, + ]); break mainLoop; } - // Here two things can happen, either the match continues so we keep on advancing the cursors if (equals(nextA, nextB)) { - bestSequence++; + segmentLength++; indexA++; indexB++; continue; } - // Or, we find a discrepancy. Before try to skip nodes to recover the match we record the current segment - segments.push([segmentAStart, segmentBStart, bestSequence]); + // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment + segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); + + // For example for + // + // 1 2 3 4 5 + // 1 2 X 4 5 + const resultA = exploreMatchBySkipping(iterA, nextA, nextB); + const resultB = exploreMatchBySkipping(iterB, nextB, nextA); + + if (resultA.length === 0 && resultB.length === 0) { + fail('0 both') + } + + if (isLatterCandidateBetter(resultA, resultB)) { + resumeWith(resultB) + } else { + resumeWith(resultA) + } + + continue mainLoop; + + function exploreMatchBySkipping( + iter: Iterator, + startAfterNode: Node, + wantedNode: Node + ): CandidateMatch { + let numberOfSkips = 0; + + // Start by skipping the current node + const from = startAfterNode.index + 1; + + // Look until we reach the skip limit or the end of the iterator, whatever happens first + const until = Math.min( + startAfterNode.index + MAX_NODE_SKIPS, + iter.nodes.length + ); - // TODO-2 This could be a source of Change type diff, maybe - // "A B C" transformed into "A b C" where "B" changed into "b" - // if (areNodesSimilar(nextA, nextB)) { - // continue - // } + let bestCandidate = getEmptyCandidate() - // We will try match the current B node with the following N nodes on A + for (const nextNodeIndex of range(from, until)) { + numberOfSkips++; - let numberOfSkips = 0; + const newCandidate = iter.peek(nextNodeIndex); - // Look until we reach the skip limit or the end of the iterator, whatever happens first - const lookForwardUntil = Math.min( - indexA + MAX_NODE_SKIPS, - iterA.nodes.length, - ); + if (!newCandidate) { + continue; + } - // Start by skipping the current node - for (const nextIndexA of range(indexA + 1, lookForwardUntil)) { - numberOfSkips++; + if (equals(newCandidate, wantedNode)) { + let candidate: CandidateMatch; + if (iter.side === Side.a) { + assert(startAfterNode.side === Side.a) + assert(wantedNode.side === Side.b) - const newCandidate = iterA.peek(nextIndexA)!; + candidate = exploreForward(nextNodeIndex, wantedNode.index, iter.side) + } else { + assert(startAfterNode.side === Side.b) + assert(wantedNode.side === Side.a) - // We found a match, so we will resume the matching in a new segment from there - if (equals(newCandidate, nextB)) { - segmentAStart = nextIndexA; - segmentBStart = indexB; - indexA = nextIndexA; - skips = numberOfSkips; - bestSequence = 0; - continue mainLoop; + candidate = exploreForward(wantedNode.index, nextNodeIndex, iter.side) + } + + if (isLatterCandidateBetter(bestCandidate, candidate)) { + bestCandidate = candidate; + } + } } + + return bestCandidate; } - // We didn't find a candidate after advancing the cursor, we are done - break; + function resumeWith(candidate: CandidateMatch) { + const [startA, startB] = candidate.segments[0]; + currentASegmentStart = startA; + currentBSegmentStart = startB; + indexA = startA; + indexB = startB; + skips += candidate.skips; + segmentLength = 0; + } } return { @@ -174,3 +226,51 @@ function getStarterNode(type: DiffType, segments: Segment[]) { return node; } + +function exploreForward( + indexA: number, + indexB: number, + skipOn: Side +): CandidateMatch { + const ogIndexA = indexA; + const ogIndexB = indexB; + + let sequenceLength = 0; + let skips = 0; + + const { iterA, iterB } = _context; + + while (true) { + const nextA = iterA.next(indexA); + const nextB = iterB.next(indexB); + + if (!nextA || !nextB) { + break; + } + + if (equals(nextA, nextB)) { + indexA++; + indexB++; + sequenceLength++; + continue; + } + + skips++; + + if (skips > MAX_NODE_SKIPS) { + break; + } + + if (skipOn === Side.a) { + indexA++; + } else { + indexB++; + } + } + + return { + segments: [[ogIndexA, ogIndexB, sequenceLength]], + length: sequenceLength, + skips, + }; +} diff --git a/src/v2/index.ts b/src/v2/index.ts index d0da9ef..b6689e2 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,7 +1,7 @@ import { Context, getIndexesFromSegment } from "./utils"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; -import { getBestMatch, getSubSequenceNodes, isNewCandidateBetter, oneSidedIteration } from "./core"; +import { getBestMatch, getSubSequenceNodes, isLatterCandidateBetter, oneSidedIteration } from "./core"; import { Change, Segment } from "./diff"; import { DiffType } from "../types"; import { fail } from "../debug"; @@ -80,7 +80,7 @@ export function getDiff2(sourceA: string, sourceB: string) { continue; } - if (isNewCandidateBetter(bestCandidate, newCandidate)) { + if (isLatterCandidateBetter(bestCandidate, newCandidate)) { bestCandidate = newCandidate; } } From 9203f533388bb8a3e1c92896c01157f154667a73 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 5 Mar 2024 12:46:01 -0300 Subject: [PATCH 14/79] wip --- src/v2/diff.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index c0fdbca..4686e4b 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -78,7 +78,8 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { const { iterA, iterB } = _context; let segmentLength = 0; - let skips = 0; + let skipsOnA = 0; + let skipsOnB = 0; let currentASegmentStart = nodeA.index; let currentBSegmentStart = nodeB.index; @@ -112,6 +113,60 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); + segmentLength = 0 + + const skipBUntil = Math.min(iterB.nodes.length, indexB + MAX_NODE_SKIPS) + + let bSkips = 0 + lookaheadB: for (const newIndexB of range(indexB, skipBUntil)) { + bSkips++ + const newB = iterA.peek(newIndexB) + + if (!newB) { + continue lookaheadB + } + + if (equals(newB, nextA)) { + // resume + + skipsOnA += bSkips + bSkips = 0 + + indexA = newIndexB + + break lookaheadB + } + } + + + + const skipAUntil = Math.min(iterA.nodes.length, indexA + MAX_NODE_SKIPS) + + let aSkips = 0 + lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { + aSkips++ + const newA = iterA.peek(newIndexA) + + if (!newA) { + continue lookaheadA + } + + if (equals(newA, nextB)) { + // resume + + skipsOnA += aSkips + aSkips = 0 + + indexA = newIndexA + + break lookaheadA + } + } + + // We skipped on A but didn't find a match, now lets consider skipping B + + + // For example for // From e4c6248770b880ab3c3c31a21d4d7522fcf65d90 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 5 Mar 2024 19:19:38 -0300 Subject: [PATCH 15/79] basic case working --- src/utils.ts | 10 ++++ src/v2/diff.ts | 143 +++++++++---------------------------------------- 2 files changed, 36 insertions(+), 117 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index e76ca40..f84f6d9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -17,6 +17,16 @@ export function* range(start: number, end: number) { } } +export function* rangeEq(start: number, end: number) { + let i = start - 1; + + while (i <= end - 1) { + i++; + yield i; + } +} + + export function oppositeSide(side: Side): Side { return side === Side.a ? Side.b : Side.a; } diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 4686e4b..ae88452 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -3,7 +3,7 @@ import { assert, fail } from "../debug"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; import { DiffType, TypeMasks } from "../types"; -import { range } from "../utils"; +import { range, rangeEq } from "../utils"; import { Node } from "./node"; import { getIndexesFromSegment } from "./utils"; import { getBestMatch, isLatterCandidateBetter } from "./core"; @@ -78,6 +78,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { const { iterA, iterB } = _context; let segmentLength = 0; + let skips = 0 let skipsOnA = 0; let skipsOnB = 0; @@ -115,136 +116,44 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); segmentLength = 0 - const skipBUntil = Math.min(iterB.nodes.length, indexB + MAX_NODE_SKIPS) + let skipsInLookaheadB = 0 - let bSkips = 0 - lookaheadB: for (const newIndexB of range(indexB, skipBUntil)) { - bSkips++ - const newB = iterA.peek(newIndexB) + const skipBUntil = Math.min(iterB.nodes.length - 1, indexB + MAX_NODE_SKIPS) - if (!newB) { - continue lookaheadB - } - - if (equals(newB, nextA)) { - // resume - - skipsOnA += bSkips - bSkips = 0 - - indexA = newIndexB - - break lookaheadB - } - } - - - - const skipAUntil = Math.min(iterA.nodes.length, indexA + MAX_NODE_SKIPS) + for (const newIndexB of rangeEq(indexB, skipBUntil)) { + const newB = iterB.next(newIndexB) - let aSkips = 0 - lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { - aSkips++ - const newA = iterA.peek(newIndexA) - - if (!newA) { - continue lookaheadA - } - - if (equals(newA, nextB)) { - // resume - - skipsOnA += aSkips - aSkips = 0 - - indexA = newIndexA - - break lookaheadA + if (!newB) { + break mainLoop } - } - // We skipped on A but didn't find a match, now lets consider skipping B + let skipsInLookaheadA = 0 + // OJO + 1 ya + const skipAUntil = Math.min(iterA.nodes.length - 1, indexA + MAX_NODE_SKIPS) + lookaheadA: for (const newIndexA of rangeEq(indexA, skipAUntil)) { + assert(newB) - + const newA = iterA.next(newIndexA) - - // For example for - // - // 1 2 3 4 5 - // 1 2 X 4 5 - const resultA = exploreMatchBySkipping(iterA, nextA, nextB); - const resultB = exploreMatchBySkipping(iterB, nextB, nextA); - - if (resultA.length === 0 && resultB.length === 0) { - fail('0 both') - } - - if (isLatterCandidateBetter(resultA, resultB)) { - resumeWith(resultB) - } else { - resumeWith(resultA) - } - - continue mainLoop; - - function exploreMatchBySkipping( - iter: Iterator, - startAfterNode: Node, - wantedNode: Node - ): CandidateMatch { - let numberOfSkips = 0; - - // Start by skipping the current node - const from = startAfterNode.index + 1; - - // Look until we reach the skip limit or the end of the iterator, whatever happens first - const until = Math.min( - startAfterNode.index + MAX_NODE_SKIPS, - iter.nodes.length - ); - - let bestCandidate = getEmptyCandidate() - - for (const nextNodeIndex of range(from, until)) { - numberOfSkips++; - - const newCandidate = iter.peek(nextNodeIndex); - - if (!newCandidate) { - continue; + if (!newA) { + break lookaheadA } - if (equals(newCandidate, wantedNode)) { - let candidate: CandidateMatch; - if (iter.side === Side.a) { - assert(startAfterNode.side === Side.a) - assert(wantedNode.side === Side.b) - - candidate = exploreForward(nextNodeIndex, wantedNode.index, iter.side) - } else { - assert(startAfterNode.side === Side.b) - assert(wantedNode.side === Side.a) + if (equals(newA, newB)) { + indexA = newIndexA + indexB = newIndexB - candidate = exploreForward(wantedNode.index, nextNodeIndex, iter.side) - } + currentASegmentStart = newIndexA + currentBSegmentStart = newIndexB - if (isLatterCandidateBetter(bestCandidate, candidate)) { - bestCandidate = candidate; - } + skips += skipsInLookaheadA + skipsInLookaheadB + continue mainLoop } - } - return bestCandidate; - } + skipsInLookaheadA++ + } - function resumeWith(candidate: CandidateMatch) { - const [startA, startB] = candidate.segments[0]; - currentASegmentStart = startA; - currentBSegmentStart = startB; - indexA = startA; - indexB = startB; - skips += candidate.skips; - segmentLength = 0; + skipsInLookaheadB++ } } From 2b8e8f4cbecfa4339ebabe14bf3bc8c88896a4e4 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 5 Mar 2024 19:29:20 -0300 Subject: [PATCH 16/79] Fix issue --- src/v2/diff.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index ae88452..9f2f695 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -118,9 +118,9 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { let skipsInLookaheadB = 0 - const skipBUntil = Math.min(iterB.nodes.length - 1, indexB + MAX_NODE_SKIPS) + const skipBUntil = Math.min(iterB.nodes.length, indexB + MAX_NODE_SKIPS) - for (const newIndexB of rangeEq(indexB, skipBUntil)) { + for (const newIndexB of range(indexB, skipBUntil)) { const newB = iterB.next(newIndexB) if (!newB) { @@ -129,8 +129,8 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { let skipsInLookaheadA = 0 // OJO + 1 ya - const skipAUntil = Math.min(iterA.nodes.length - 1, indexA + MAX_NODE_SKIPS) - lookaheadA: for (const newIndexA of rangeEq(indexA, skipAUntil)) { + const skipAUntil = Math.min(iterA.nodes.length, indexA + MAX_NODE_SKIPS) + lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { assert(newB) const newA = iterA.next(newIndexA) From 77f19e1c4e7f72e1df633c7d27584f0692d14332 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 5 Mar 2024 19:57:03 -0300 Subject: [PATCH 17/79] rough but working on B --- src/v2/diff.ts | 62 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 9f2f695..0df317b 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -118,6 +118,22 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { let skipsInLookaheadB = 0 + function getSameNodesAhead(iter: Iterator, wantedNode: Node, lastIndex: number) { + const nodes: Node[] = [] + for (const index of rangeEq(wantedNode.index + 1, lastIndex)) { + const next = iter.next(index) + + if (!next) { + continue + } + + if (equals(wantedNode, next)) { + nodes.push(next) + } + } + return nodes + } + const skipBUntil = Math.min(iterB.nodes.length, indexB + MAX_NODE_SKIPS) for (const newIndexB of range(indexB, skipBUntil)) { @@ -140,14 +156,44 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { } if (equals(newA, newB)) { - indexA = newIndexA - indexB = newIndexB - - currentASegmentStart = newIndexA - currentBSegmentStart = newIndexB - - skips += skipsInLookaheadA + skipsInLookaheadB - continue mainLoop + const sameNodesAhead = getSameNodesAhead(iterB, newB, skipBUntil) + + if (sameNodesAhead.length) { + let bestCandidate = getEmptyCandidate(); + + for (const node of sameNodesAhead) { + const newCandidate = getBestMatch(node); + + if (!newCandidate) { + fail() + } + + if (isLatterCandidateBetter(bestCandidate, newCandidate)) { + bestCandidate = newCandidate; + } + } + + const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]) + + indexA = a.start + indexB = b.start + + currentASegmentStart = a.start + currentBSegmentStart = b.start + + skips += skipsInLookaheadA + skipsInLookaheadB + + continue mainLoop + } else { + indexA = newIndexA + indexB = newIndexB + + currentASegmentStart = newIndexA + currentBSegmentStart = newIndexB + + skips += skipsInLookaheadA + skipsInLookaheadB + continue mainLoop + } } skipsInLookaheadA++ From 6e56d8b961bc72ef1020b138a0fc6e7c49aa4af4 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 11 Mar 2024 16:32:44 -0300 Subject: [PATCH 18/79] New debug tool --- src/utils.ts | 1 - src/v2/printer.ts | 90 ++++++++++++++++++++++++++++++++++++----------- src/v2/utils.ts | 4 +++ 3 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index f84f6d9..813004b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -26,7 +26,6 @@ export function* rangeEq(start: number, end: number) { } } - export function oppositeSide(side: Side): Side { return side === Side.a ? Side.b : Side.a; } diff --git a/src/v2/printer.ts b/src/v2/printer.ts index 6e422c7..09582ff 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -1,10 +1,13 @@ -import { asciiRenderFn, assert, createTextTable, fail, RenderFn } from "../debug"; -import { DiffType } from "../types"; +import Table from "cli-table3"; +import colorFn from "kleur"; + +import { asciiRenderFn, assert, fail, prettyRenderFn, RenderFn } from "../debug"; +import { DiffType, TypeMasks } from "../types"; import { Change } from "./diff"; -import { getIndexesFromSegment } from "./utils"; +import { capitalizeFirstLetter, getIndexesFromSegment } from "./utils"; import { Iterator } from "./iterator"; import { _context } from "."; -import colorFn from "kleur"; +import { rangeEq } from "../utils"; export function applyChangesToSources( sourceA: string, @@ -213,13 +216,26 @@ export function getPrettyStringFromChange( for (const segment of change.segments) { // switch diff type render add del move separtely const { a, b } = getIndexesFromSegment(segment); - const [startA, endA] = getStringPositionsFromRange(iterA, a.start, a.end - 1); - const [startB, endB] = getStringPositionsFromRange(iterB, b.start, b.end - 1); - - const color = getColor(); + + const color = prettyRenderFn[change.type]; + + if (change.type & TypeMasks.DelOrMove) { + const [startA, endA] = getStringPositionsFromRange( + iterA, + a.start, + a.end - 1, + ); + charsA = getSourceWithChange(charsA, startA, endA, color); + } - charsA = getSourceWithChange(charsA, startA, endA, color); - charsB = getSourceWithChange(charsB, startB, endB, color); + if (change.type & TypeMasks.AddOrMove) { + const [startB, endB] = getStringPositionsFromRange( + iterB, + b.start, + b.end - 1, + ); + charsB = getSourceWithChange(charsB, startB, endB, color); + } } return { @@ -229,18 +245,52 @@ export function getPrettyStringFromChange( } export function prettyPrintChanges(a: string, b: string, changes: Change[]) { - let sequenceCounter = -1; - for (const change of changes) { - sequenceCounter++; - console.log( - `\n---------- Starter ${change.startNode.prettyKind} ${`"${change.startNode.text}"` || ""} Length: ${change.length} Segments ${change.segments.length} Skips: ${change.skips} ----------\n`, - ); - const sourcesWithColor = getPrettyStringFromChange(change, a, b); + const table = new Table({ + head: [ + colorFn.magenta("Type"), + colorFn.blue("Length"), + colorFn.grey("Skips"), + colorFn.cyan("Start"), + colorFn.yellow("Line Nº"), + colorFn.red("Source"), + colorFn.green("Revision"), + ], + colAligns: ["center", "center", "center", "center"], + }); + + const sortedByLength = changes.sort((a, b) => (a.length < b.length ? 1 : -1)); + + const lineNumberString = getLinesOfCodeString(a, b); + + for (const change of sortedByLength) { + const changeName = capitalizeFirstLetter(DiffType[change.type]); + const changeColorFn = prettyRenderFn[change.type]; + + const sourceWithChange = getPrettyStringFromChange(change, a, b); + + table.push([ + changeColorFn(changeName), + colorFn.blue(change.length), + colorFn.grey(change.skips || "-"), + colorFn.cyan(`"${change.startNode.text}"`), + colorFn.yellow(lineNumberString), + sourceWithChange.a, + sourceWithChange.b, + ]); + } - const table = createTextTable(sourcesWithColor.a, sourcesWithColor.b); + console.log(table.toString()); +} - console.log(table); +function getLinesOfCodeString(a: string, b: string) { + const aLines = a.split("\n"); + const bLines = b.split("\n"); + const linesOfCode = Math.max(aLines.length, bLines.length); - resetColorRoulette(); + let str = ""; + for (const lineNumber of rangeEq(1, linesOfCode)) { + str += `${lineNumber}\n`; } + + return str; } diff --git a/src/v2/utils.ts b/src/v2/utils.ts index ac84940..2b891ba 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -55,3 +55,7 @@ export function getIndexesFromSegment(segment: Segment) { }, }; } + +export function capitalizeFirstLetter(string: string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} From 67982428cdd0762e4fa4c413aadf89b558bdba37 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 12 Mar 2024 17:22:12 -0300 Subject: [PATCH 19/79] Improved move finder logic --- src/v2/diff.ts | 146 +++++++++++++++++++++++++++------------------- src/v2/printer.ts | 2 +- 2 files changed, 87 insertions(+), 61 deletions(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 0df317b..630b14d 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -31,7 +31,7 @@ export class Change { constructor( public type: DiffType, public segments: Segment[], - public skips = 0 + public skips = 0, ) { this.startNode = getStarterNode(type, segments); this.length = calculateCandidateMatchLength(segments); @@ -78,7 +78,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { const { iterA, iterB } = _context; let segmentLength = 0; - let skips = 0 + let skips = 0; let skipsOnA = 0; let skipsOnB = 0; @@ -114,92 +114,118 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); - segmentLength = 0 - - let skipsInLookaheadB = 0 - - function getSameNodesAhead(iter: Iterator, wantedNode: Node, lastIndex: number) { - const nodes: Node[] = [] + segmentLength = 0; + + let skipsInLookaheadB = 0; + + // Verify the following n number of nodes ahead storing the ones equals to the wanted node + // + // A B C B C B B + // 0 1 2 3 4 5 6 + // + // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6 + function getSameNodesAhead( + iter: Iterator, + wantedNode: Node, + lastIndex: number, + ) { + const nodes: Node[] = []; for (const index of rangeEq(wantedNode.index + 1, lastIndex)) { - const next = iter.next(index) + const next = iter.next(index); if (!next) { - continue + continue; } if (equals(wantedNode, next)) { - nodes.push(next) + nodes.push(next); } } - return nodes + return nodes; } - const skipBUntil = Math.min(iterB.nodes.length, indexB + MAX_NODE_SKIPS) + const skipBUntil = Math.min(iterB.nodes.length, indexB + MAX_NODE_SKIPS); for (const newIndexB of range(indexB, skipBUntil)) { - const newB = iterB.next(newIndexB) + const newB = iterB.next(newIndexB); if (!newB) { - break mainLoop + break mainLoop; } - let skipsInLookaheadA = 0 - // OJO + 1 ya - const skipAUntil = Math.min(iterA.nodes.length, indexA + MAX_NODE_SKIPS) + let skipsInLookaheadA = 0; + + const skipAUntil = Math.min(iterA.nodes.length, indexA + MAX_NODE_SKIPS); lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { - assert(newB) + assert(newB); - const newA = iterA.next(newIndexA) + const newA = iterA.next(newIndexA); if (!newA) { - break lookaheadA + break lookaheadA; } if (equals(newA, newB)) { - const sameNodesAhead = getSameNodesAhead(iterB, newB, skipBUntil) - - if (sameNodesAhead.length) { - let bestCandidate = getEmptyCandidate(); - - for (const node of sameNodesAhead) { - const newCandidate = getBestMatch(node); - - if (!newCandidate) { - fail() - } - - if (isLatterCandidateBetter(bestCandidate, newCandidate)) { - bestCandidate = newCandidate; - } + const sameNodesAheadA = getSameNodesAhead(iterA, newA, skipAUntil); + const sameNodesAheadB = getSameNodesAhead(iterB, newB, skipBUntil); + + if (!sameNodesAheadA.length && !sameNodesAheadB.length) { + indexA = newIndexA; + indexB = newIndexB; + + currentASegmentStart = newIndexA; + currentBSegmentStart = newIndexB; + + skips += skipsInLookaheadA + skipsInLookaheadB; + continue mainLoop; + } + + let bestCandidate = getEmptyCandidate(); + + // TODO instead of getBestMatch use getCandidateMatch so that it skips node if necessary + // TODO also getBestMatch has hardcoded B side + + for (const node of sameNodesAheadA) { + const newCandidate = getBestMatch(node); + + if (!newCandidate) { + fail(); } - const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]) - - indexA = a.start - indexB = b.start - - currentASegmentStart = a.start - currentBSegmentStart = b.start - - skips += skipsInLookaheadA + skipsInLookaheadB - - continue mainLoop - } else { - indexA = newIndexA - indexB = newIndexB - - currentASegmentStart = newIndexA - currentBSegmentStart = newIndexB - - skips += skipsInLookaheadA + skipsInLookaheadB - continue mainLoop - } + if (isLatterCandidateBetter(bestCandidate, newCandidate)) { + bestCandidate = newCandidate; + } + } + + for (const node of sameNodesAheadB) { + const newCandidate = getBestMatch(node); + + if (!newCandidate) { + fail(); + } + + if (isLatterCandidateBetter(bestCandidate, newCandidate)) { + bestCandidate = newCandidate; + } + } + + const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]); + + indexA = a.start; + indexB = b.start; + + currentASegmentStart = a.start; + currentBSegmentStart = b.start; + + skips += skipsInLookaheadA + skipsInLookaheadB; + + continue mainLoop; } - skipsInLookaheadA++ + skipsInLookaheadA++; } - skipsInLookaheadB++ + skipsInLookaheadB++; } } @@ -240,7 +266,7 @@ function getStarterNode(type: DiffType, segments: Segment[]) { function exploreForward( indexA: number, indexB: number, - skipOn: Side + skipOn: Side, ): CandidateMatch { const ogIndexA = indexA; const ogIndexB = indexB; diff --git a/src/v2/printer.ts b/src/v2/printer.ts index 09582ff..fa2701f 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -216,7 +216,7 @@ export function getPrettyStringFromChange( for (const segment of change.segments) { // switch diff type render add del move separtely const { a, b } = getIndexesFromSegment(segment); - + const color = prettyRenderFn[change.type]; if (change.type & TypeMasks.DelOrMove) { From 5877369c5039cc7c39bd80c56dd1897f5821503e Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 13 Mar 2024 13:26:33 -0300 Subject: [PATCH 20/79] Properly track skips on nodesAhead branch --- src/v2/diff.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 630b14d..c2651f4 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -193,6 +193,14 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { } if (isLatterCandidateBetter(bestCandidate, newCandidate)) { + // Give this input + // A B C C D + // 0 1 2 3 4 + // + // Lets say we need to pick between one of the "C", picking the first one will mean no skip, picking the second one will imply one skip + // 1: A B C + // 2: A B _ C + skipsInLookaheadA += node.index - newA.index bestCandidate = newCandidate; } } @@ -205,6 +213,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { } if (isLatterCandidateBetter(bestCandidate, newCandidate)) { + skipsInLookaheadB += node.index - newB.index bestCandidate = newCandidate; } } From de1861fc6d6e6fd42ed5cba2eb02d35fa614d964 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 13 Mar 2024 13:26:44 -0300 Subject: [PATCH 21/79] Added util --- src/v2/iterator.ts | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index 9beaed9..d503768 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -1,9 +1,11 @@ +import Table from "cli-table3"; +import colorFn from "kleur"; + import { _context } from "."; import { getSourceWithChange } from "../backend/printer"; import { Side } from "../shared/language"; import { getTsNodes } from "./compilers"; import { Node } from "./node"; -import colorFn from "kleur"; import { NodesTable } from "./types"; import { DiffType } from "../types"; @@ -86,4 +88,44 @@ export class Iterator { console.log(result.join("")); } + + printNodes() { + const table = new Table({ + head: [ + colorFn.blue("Index"), + colorFn.yellow("Text"), + colorFn.red("Kind"), + ], + colAligns: ["center", "center", "center"], + }); + + for (const node of this.nodes) { + let color; + switch (node.markedAs) { + case DiffType.addition: { + color = colorFn.green; + break; + } + case DiffType.deletion: { + color = colorFn.red; + break; + } + case DiffType.move: { + color = colorFn.blue; + break; + } + default: { + color = colorFn.grey; + } + } + + table.push([ + color(node.index), + color(node.text), + color(node.prettyKind) + ]) + } + + console.log(table.toString()) + } } From fb2c7f1e57df9e90dbbaba40b89564882d80ca7d Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 13 Mar 2024 13:27:57 -0300 Subject: [PATCH 22/79] Wip --- src/v2/diff.ts | 6 ++-- src/v2/index.ts | 34 ++++++++++++++------ src/v2/iterator.ts | 6 ++-- src/v2/printer.ts | 13 ++++++-- src/v2/types.ts | 17 ++++++++++ src/v2/utils.ts | 2 +- tests/utils2.ts | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 tests/utils2.ts diff --git a/src/v2/diff.ts b/src/v2/diff.ts index c2651f4..e10e683 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -183,7 +183,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { let bestCandidate = getEmptyCandidate(); // TODO instead of getBestMatch use getCandidateMatch so that it skips node if necessary - // TODO also getBestMatch has hardcoded B side + // TODO also getBestMatch has hardcoded B side for (const node of sameNodesAheadA) { const newCandidate = getBestMatch(node); @@ -200,7 +200,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { // Lets say we need to pick between one of the "C", picking the first one will mean no skip, picking the second one will imply one skip // 1: A B C // 2: A B _ C - skipsInLookaheadA += node.index - newA.index + skipsInLookaheadA += node.index - newA.index; bestCandidate = newCandidate; } } @@ -213,7 +213,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { } if (isLatterCandidateBetter(bestCandidate, newCandidate)) { - skipsInLookaheadB += node.index - newB.index + skipsInLookaheadB += node.index - newB.index; bestCandidate = newCandidate; } } diff --git a/src/v2/index.ts b/src/v2/index.ts index b6689e2..1481993 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -2,13 +2,20 @@ import { Context, getIndexesFromSegment } from "./utils"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; import { getBestMatch, getSubSequenceNodes, isLatterCandidateBetter, oneSidedIteration } from "./core"; -import { Change, Segment } from "./diff"; +import { Change } from "./diff"; import { DiffType } from "../types"; -import { fail } from "../debug"; +import { asciiRenderFn, fail, prettyRenderFn } from "../debug"; import { range } from "../utils"; -import { Diff } from "../data_structures/diff"; +import { Options, OutputType, ResultTypeMapper } from "./types"; +import { applyChangesToSources } from "./printer"; + +const defaultOptions: Options = { + outputType: OutputType.changes, +}; + +export function getDiff2<_OutputType extends OutputType = OutputType.changes>(sourceA: string, sourceB: string, options?: Options<_OutputType>): ResultTypeMapper[_OutputType] { + const _options = { ...defaultOptions, ...(options || {}) }; -export function getDiff2(sourceA: string, sourceB: string) { const iterA = new Iterator(sourceA, Side.a); const iterB = new Iterator(sourceB, Side.b); @@ -61,12 +68,9 @@ export function getDiff2(sourceA: string, sourceB: string) { continue; } + // May be empty if the node we are looking for was the only one const subSequenceNodesToCheck = getSubSequenceNodes(bestMatchForB, b); - if (!subSequenceNodesToCheck.length) { - fail("IDK"); - } - let bestCandidate = bestMatchForB; for (const node of subSequenceNodesToCheck) { @@ -91,7 +95,19 @@ export function getDiff2(sourceA: string, sourceB: string) { continue; } - return changes; + switch (_options.outputType) { + case OutputType.changes: { + return changes as ResultTypeMapper[_OutputType]; + } + case OutputType.text: { + return applyChangesToSources(sourceA, sourceB, changes, asciiRenderFn) as ResultTypeMapper[_OutputType]; + } + case OutputType.prettyText: { + return applyChangesToSources(sourceA, sourceB, changes, prettyRenderFn) as ResultTypeMapper[_OutputType]; + } + default: + fail(); + } } function markMatched(change: Change) { diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index d503768..9a6b2e9 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -122,10 +122,10 @@ export class Iterator { table.push([ color(node.index), color(node.text), - color(node.prettyKind) - ]) + color(node.prettyKind), + ]); } - console.log(table.toString()) + console.log(table.toString()); } } diff --git a/src/v2/printer.ts b/src/v2/printer.ts index fa2701f..2e682ed 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -1,7 +1,7 @@ import Table from "cli-table3"; import colorFn from "kleur"; -import { asciiRenderFn, assert, fail, prettyRenderFn, RenderFn } from "../debug"; +import { asciiRenderFn, assert, createTextTable, fail, prettyRenderFn, RenderFn } from "../debug"; import { DiffType, TypeMasks } from "../types"; import { Change } from "./diff"; import { capitalizeFirstLetter, getIndexesFromSegment } from "./utils"; @@ -153,7 +153,7 @@ export function getSourceWithChange( .map((x) => colorFn(x)) .join(" "); - return [...head, colorFn(text), ...compliment, ...tail]; + return [...head, text, ...compliment, ...tail]; } export function getComplimentArray( @@ -244,7 +244,16 @@ export function getPrettyStringFromChange( }; } +export function prettyPrintSources(a: string, b: string) { + console.log(createTextTable(a, b)); +} + export function prettyPrintChanges(a: string, b: string, changes: Change[]) { + const sourcesWithChanges = applyChangesToSources(a, b, changes, prettyRenderFn); + console.log(createTextTable(sourcesWithChanges.sourceA, sourcesWithChanges.sourceB)); +} + +export function prettyPrintChangesInSequence(a: string, b: string, changes: Change[]) { const table = new Table({ head: [ colorFn.magenta("Type"), diff --git a/src/v2/types.ts b/src/v2/types.ts index 80e56b7..cb031f7 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -1,4 +1,5 @@ import { DiffType } from "../types"; +import { Change } from "./diff"; import { Node } from "./node"; export type SyntaxKind = number; @@ -10,3 +11,19 @@ export interface ParsedProgram { allNodes: Node[]; nodesTable: NodesTable; } + +export interface Options<_OutputType extends OutputType = OutputType.changes> { + outputType: _OutputType; +} + +export enum OutputType { + changes, + text, + prettyText, +} + +export interface ResultTypeMapper { + [OutputType.changes]: Change[]; + [OutputType.text]: { sourceA: string; sourceB: string }; + [OutputType.prettyText]: { sourceA: string; sourceB: string }; +} diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 2b891ba..8947060 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -2,7 +2,7 @@ import { _context } from "."; import { assert } from "../debug"; import { Side } from "../shared/language"; import { range } from "../utils"; -import { CandidateMatch, Change, Segment } from "./diff"; +import { CandidateMatch, Segment } from "./diff"; import { Iterator } from "./iterator"; import { Node } from "./node"; diff --git a/tests/utils2.ts b/tests/utils2.ts new file mode 100644 index 0000000..ea93994 --- /dev/null +++ b/tests/utils2.ts @@ -0,0 +1,78 @@ +import { expect, test as vTest } from "vitest"; +import { getDiff2 } from "../src/v2/index"; +import { OutputType } from "../src/v2/types"; + +interface TestInfo { + disabled?: boolean; + only?: "standard" | "inversed"; + name?: string | number; + a?: string; + b?: string; + expA?: string; + expB?: string; +} + +export function getTestFn(testFn: typeof getDiff2) { + return function test(testInfo: TestInfo) { + const { a = "", b = "", expA, expB, name = "anonymous" } = testInfo; + + if (a === expA && b === expB) { + throw new Error(`Invalid test ${name}, input and output are the same`); + } + + if (testInfo.disabled) { + return; + } + + const skipStandardTest = testInfo.only === "inversed"; + + if (!skipStandardTest) { + vTest(`Test ${name}`, () => { + const { sourceA: resultA, sourceB: resultB } = testFn(a, b, { outputType: OutputType.text }); + + validateDiff(expA || a, expB || b, resultA, resultB); + }); + } + + const skipInversedTest = testInfo.only === "standard"; + + if (!skipInversedTest) { + vTest(`Test ${name} inverse`, () => { + const { sourceA: resultA, sourceB: resultB } = testFn(b, a, { outputType: OutputType.text }); + + const inversedExpectedA = getInversedExpectedResult(expB || b); + const inversedExpectedB = getInversedExpectedResult(expA || a); + + validateDiff(inversedExpectedA, inversedExpectedB, resultA, resultB); + }); + } + }; +} + +export function getInversedExpectedResult(expected: string) { + return expected.split("").map((char) => { + if (char === "➖") { + return "➕"; + } else if (char === "➕") { + return "➖"; + } else { + return char; + } + }).join(""); +} + +export const test = getTestFn(getDiff2); + +export function validateDiff( + expectedA: string, + expectedB: string, + resultA: string, + resultB: string, +) { + function trimLines(text: string) { + return text.split("\n").map((s) => s.trim()).join(""); + } + + expect(trimLines(resultA)).toEqual(trimLines(expectedA)); + expect(trimLines(resultB)).toEqual(trimLines(expectedB)); +} From 0eda648fbe5b767704b007c0523a69fdef5c6711 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 13 Mar 2024 16:33:02 -0300 Subject: [PATCH 23/79] Fix logic bug --- src/v2/core.ts | 4 +-- src/v2/diff.ts | 72 +++++++++++--------------------------------------- 2 files changed, 17 insertions(+), 59 deletions(-) diff --git a/src/v2/core.ts b/src/v2/core.ts index 66b49c4..b0894fe 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -90,8 +90,8 @@ export function isLatterCandidateBetter(currentCandidate: CandidateMatch, newCan return false; } - if (newCandidate.segments.length > currentCandidate.segments.length) { - return false; + if (newCandidate.segments.length < currentCandidate.segments.length) { + return true; } else if (newCandidate.segments.length < currentCandidate.segments.length) { return false; } diff --git a/src/v2/diff.ts b/src/v2/diff.ts index e10e683..0deec6d 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -38,7 +38,7 @@ export class Change { } static createAddition(node: Node) { - assert(node.side === Side.b); + assert(node.side === Side.b, () => "Trying to create a deletion but received an A side node"); return new Change(DiffType.addition, [ [ // Start A @@ -52,7 +52,7 @@ export class Change { } static createDeletion(node: Node) { - assert(node.side === Side.a); + assert(node.side === Side.a, () => "Trying to create a deletion but received an B side node"); return new Change(DiffType.deletion, [ [ // Start A @@ -79,8 +79,6 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { let segmentLength = 0; let skips = 0; - let skipsOnA = 0; - let skipsOnB = 0; let currentASegmentStart = nodeA.index; let currentBSegmentStart = nodeB.index; @@ -92,11 +90,13 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { mainLoop: while (true) { // TODO-2 First iteration already has the nodes - const nextA = iterA.next(indexA); - const nextB = iterB.next(indexB); + const nextA = iterA.peek(indexA); + const nextB = iterB.peek(indexB); // If one of the iterators ends then there is no more search to do if (!nextA || !nextB) { + assert(segmentLength > 0, () => "Segment length is 0") + segments.push([ currentASegmentStart, currentBSegmentStart, @@ -112,6 +112,8 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { continue; } + assert(segmentLength > 0, () => "Segment length is 0") + // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); segmentLength = 0; @@ -131,7 +133,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { ) { const nodes: Node[] = []; for (const index of rangeEq(wantedNode.index + 1, lastIndex)) { - const next = iter.next(index); + const next = iter.peek(index); if (!next) { continue; @@ -147,7 +149,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { const skipBUntil = Math.min(iterB.nodes.length, indexB + MAX_NODE_SKIPS); for (const newIndexB of range(indexB, skipBUntil)) { - const newB = iterB.next(newIndexB); + const newB = iterB.peek(newIndexB); if (!newB) { break mainLoop; @@ -159,7 +161,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { assert(newB); - const newA = iterA.next(newIndexA); + const newA = iterA.peek(newIndexA); if (!newA) { break lookaheadA; @@ -236,6 +238,8 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { skipsInLookaheadB++; } + + break mainLoop } return { @@ -256,6 +260,8 @@ export function calculateCandidateMatchLength(segments: Segment[]) { sum += segment[2]; } + assert(sum > 0, () => "Segment length is 0") + return sum; } @@ -271,51 +277,3 @@ function getStarterNode(type: DiffType, segments: Segment[]) { return node; } - -function exploreForward( - indexA: number, - indexB: number, - skipOn: Side, -): CandidateMatch { - const ogIndexA = indexA; - const ogIndexB = indexB; - - let sequenceLength = 0; - let skips = 0; - - const { iterA, iterB } = _context; - - while (true) { - const nextA = iterA.next(indexA); - const nextB = iterB.next(indexB); - - if (!nextA || !nextB) { - break; - } - - if (equals(nextA, nextB)) { - indexA++; - indexB++; - sequenceLength++; - continue; - } - - skips++; - - if (skips > MAX_NODE_SKIPS) { - break; - } - - if (skipOn === Side.a) { - indexA++; - } else { - indexB++; - } - } - - return { - segments: [[ogIndexA, ogIndexB, sequenceLength]], - length: sequenceLength, - skips, - }; -} From d2407376b2137905872c706379eba54db76d0d4d Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 13 Mar 2024 16:33:21 -0300 Subject: [PATCH 24/79] Wip --- src/v2/index.ts | 6 +++--- src/v2/printer.ts | 4 ++-- src/v2/utils.ts | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/v2/index.ts b/src/v2/index.ts index 1481993..c585633 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -29,13 +29,13 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so // console.log(i, "|", x.start, x.end, x.prettyKind, `"${x.text}"`) // }) - _context = new Context(sourceA, sourceB); + const changes: Change[] = []; + + _context = new Context(sourceA, sourceB, changes); _context.iterA = iterA; _context.iterB = iterB; - const changes: Change[] = []; - while (true) { const a = iterA.next(); const b = iterB.next(); diff --git a/src/v2/printer.ts b/src/v2/printer.ts index 2e682ed..e7c03c9 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -253,7 +253,7 @@ export function prettyPrintChanges(a: string, b: string, changes: Change[]) { console.log(createTextTable(sourcesWithChanges.sourceA, sourcesWithChanges.sourceB)); } -export function prettyPrintChangesInSequence(a: string, b: string, changes: Change[]) { +export function prettyPrintChangesInSequence(a: string, b: string, changes: Change[], options: { sortByLength: boolean } = { sortByLength: true }) { const table = new Table({ head: [ colorFn.magenta("Type"), @@ -267,7 +267,7 @@ export function prettyPrintChangesInSequence(a: string, b: string, changes: Chan colAligns: ["center", "center", "center", "center"], }); - const sortedByLength = changes.sort((a, b) => (a.length < b.length ? 1 : -1)); + const sortedByLength = options.sortByLength ? changes.sort((a, b) => (a.length < b.length ? 1 : -1)) : changes; const lineNumberString = getLinesOfCodeString(a, b); diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 8947060..995e315 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -2,7 +2,7 @@ import { _context } from "."; import { assert } from "../debug"; import { Side } from "../shared/language"; import { range } from "../utils"; -import { CandidateMatch, Segment } from "./diff"; +import { CandidateMatch, Change, Segment } from "./diff"; import { Iterator } from "./iterator"; import { Node } from "./node"; @@ -14,6 +14,7 @@ export class Context { constructor( public sourceA: string, public sourceB: string, + public changes: Change[] ) {} } From dac5f2da7bf86a9bfddf4e63d9534637e7899ac5 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Thu, 14 Mar 2024 20:22:42 -0300 Subject: [PATCH 25/79] Moved code around --- src/debug.ts | 4 -- src/v2/change.ts | 65 ++++++++++++++++++++++++ src/v2/core.ts | 121 +++++++++++++++++++++++-------------------- src/v2/diff.ts | 127 +++++++++++++++------------------------------- src/v2/index.ts | 101 +++--------------------------------- src/v2/printer.ts | 2 +- src/v2/types.ts | 15 ++++-- src/v2/utils.ts | 52 +++++++++++++++++-- 8 files changed, 240 insertions(+), 247 deletions(-) create mode 100644 src/v2/change.ts diff --git a/src/debug.ts b/src/debug.ts index ae0b290..a15a277 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -2,11 +2,7 @@ import ts from "typescript"; import Table from "cli-table3"; import { DiffType } from "./types"; import colorFn from "kleur"; -import { getSourceWithChange } from "./backend/printer"; import { _context as _context2 } from "./v2/index"; -import { Iterator } from "./v2/iterator"; -import { getIndexesFromSegment } from "./v2/utils"; -import { Change } from "./v2/diff"; enum ErrorType { DebugFailure = "DebugFailure", diff --git a/src/v2/change.ts b/src/v2/change.ts new file mode 100644 index 0000000..bac271f --- /dev/null +++ b/src/v2/change.ts @@ -0,0 +1,65 @@ +import { Node } from "./node"; +import { Side } from "../shared/language"; +import { DiffType, TypeMasks } from "../types"; +import { CandidateMatch, Segment } from "./types"; +import { calculateCandidateMatchLength, getIndexesFromSegment } from "./utils"; +import { assert } from "../debug"; +import { _context } from "."; + +export class Change { + startNode: Node; + length: number; + constructor( + public type: DiffType, + public segments: Segment[], + public skips = 0, + ) { + this.startNode = getStarterNode(type, segments); + this.length = calculateCandidateMatchLength(segments); + } + + static createAddition(node: Node) { + assert(node.side === Side.b, () => "Trying to create a deletion but received an A side node"); + return new Change(DiffType.addition, [ + [ + // Start A + -1, + // Start B + node.index, + // Length + 1, + ], + ]); + } + + static createDeletion(node: Node) { + assert(node.side === Side.a, () => "Trying to create a deletion but received an B side node"); + return new Change(DiffType.deletion, [ + [ + // Start A + node.index, + // Start B + -1, + // Length + 1, + ], + ]); + } + + static createMove(match: CandidateMatch) { + return new Change(DiffType.move, match.segments, match.skips); + } +} + +function getStarterNode(type: DiffType, segments: Segment[]) { + const { a, b } = getIndexesFromSegment(segments[0]); + + const iter = type & TypeMasks.AddOrMove ? _context.iterB : _context.iterA; + const index = type & TypeMasks.AddOrMove ? b.start : a.start; + + const node = iter.nodes[index]; + + assert(node, () => "Failed to get starter node"); + + return node; +} diff --git a/src/v2/core.ts b/src/v2/core.ts index b0894fe..d133b2d 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -1,62 +1,92 @@ import { _context } from "."; -import { SyntaxKind } from "./types"; -import { Node } from "./node"; -import { CandidateMatch, Change, getCandidateMatch } from "./diff"; -import { getAllNodesFromMatch } from "./utils"; -import { fail } from "../debug"; +import { getBestMatch, getSubSequenceNodes } from "./diff"; +import { getIndexesFromSegment, isLatterCandidateBetter } from "./utils"; import { Iterator } from "./iterator"; import { DiffType } from "../types"; +import { Change } from "./change"; +import { range } from "../utils"; -/** - * Returns the longest possible match for the given node, this is including possible skips to improve the match length. - */ -export function getBestMatch(nodeB: Node): CandidateMatch | undefined { - const aSideCandidates = _context.iterA.getMatchingNodes(nodeB); +export function computeDiff() { + const { iterA, iterB, changes } = _context; - // The given B node wasn't found, it was added - if (aSideCandidates.length === 0) { - return; - } + while (true) { + const a = iterA.next(); + const b = iterB.next(); - let bestMatch: CandidateMatch = { - length: 0, - skips: 0, - segments: [], - }; + // No more nodes to process. We are done + if (!a && !b) { + break; + } - for (const candidate of aSideCandidates) { - const newCandidate = getCandidateMatch(candidate, nodeB); + // One of the iterators ran out of nodes. We will mark the remaining unmatched nodes + if (!a || !b) { + // If A finished means that B still have nodes, they are additions. If B finished means that A have nodes, they are deletions + const iterOn = !a ? iterB : iterA; + const type = !a ? DiffType.addition : DiffType.deletion; - if (isLatterCandidateBetter(bestMatch, newCandidate)) { - bestMatch = newCandidate; + const remainingChanges = oneSidedIteration(iterOn, type); + changes.push(...remainingChanges); + break; } - } - return bestMatch; -} + // 1- + const bestMatchForB = getBestMatch(b); + + if (!bestMatchForB) { + const addition = Change.createAddition(b); + changes.push(addition); + iterB.mark(b.index, DiffType.addition); + + continue; + } + + // May be empty if the node we are looking for was the only one + const subSequenceNodesToCheck = getSubSequenceNodes(bestMatchForB, b); + + let bestCandidate = bestMatchForB; -export function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { - const nodesInMatch = getAllNodesFromMatch(match); + for (const node of subSequenceNodesToCheck) { + const newCandidate = getBestMatch(node); - const allNodes = new Set(); + if (!newCandidate) { + const addition = Change.createAddition(b); + changes.push(addition); + iterB.mark(b.index, DiffType.addition); - for (const node of nodesInMatch) { - const similarNodes = _context.iterB.getMatchingNodes(node); + continue; + } - for (const _node of similarNodes) { - allNodes.add(_node); + if (isLatterCandidateBetter(bestCandidate, newCandidate)) { + bestCandidate = newCandidate; + } } + + const move = Change.createMove(bestCandidate); + changes.push(move); + markMatched(move); + continue; } - allNodes.delete(starterNode); + return changes; +} - return [...allNodes]; +function markMatched(change: Change) { + for (const segment of change.segments) { + const { a, b } = getIndexesFromSegment(segment); + + for (const index of range(a.start, a.end)) { + _context.iterA.mark(index, change.type); + } + + for (const index of range(b.start, b.end)) { + _context.iterB.mark(index, change.type); + } + } } export function oneSidedIteration( iter: Iterator, typeOfChange: DiffType.addition | DiffType.deletion, - startFrom: number, ): Change[] { const changes: Change[] = []; @@ -79,22 +109,3 @@ export function oneSidedIteration( return changes; } - -/** - * TODO: MAybe compute a score fn - */ -export function isLatterCandidateBetter(currentCandidate: CandidateMatch, newCandidate: CandidateMatch): boolean { - if (newCandidate.length > currentCandidate.length) { - return true; - } else if (newCandidate.length < currentCandidate.length) { - return false; - } - - if (newCandidate.segments.length < currentCandidate.segments.length) { - return true; - } else if (newCandidate.segments.length < currentCandidate.segments.length) { - return false; - } - - return newCandidate.skips < currentCandidate.skips; -} diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 0deec6d..43991a9 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -1,73 +1,55 @@ import { _context } from "."; import { assert, fail } from "../debug"; -import { Side } from "../shared/language"; import { Iterator } from "./iterator"; -import { DiffType, TypeMasks } from "../types"; import { range, rangeEq } from "../utils"; import { Node } from "./node"; -import { getIndexesFromSegment } from "./utils"; -import { getBestMatch, isLatterCandidateBetter } from "./core"; - -// Start is inclusive, end is not inclusive -export type Segment = [indexA: number, indexB: number, length: number]; - -export interface CandidateMatch { - length: number; - skips: number; - segments: Segment[]; -} +import { calculateCandidateMatchLength, equals, getAllNodesFromMatch, getEmptyCandidate, getIndexesFromSegment, isLatterCandidateBetter } from "./utils"; +import { CandidateMatch, Segment } from "./types"; + +/** + * Returns the longest possible match for the given node, this is including possible skips to improve the match length. + */ +export function getBestMatch(nodeB: Node): CandidateMatch | undefined { + const aSideCandidates = _context.iterA.getMatchingNodes(nodeB); + + // The given B node wasn't found, it was added + if (aSideCandidates.length === 0) { + return; + } -export function getEmptyCandidate(): CandidateMatch { - return { + let bestMatch: CandidateMatch = { length: 0, - segments: [], skips: 0, + segments: [], }; -} -export class Change { - startNode: Node; - length: number; - constructor( - public type: DiffType, - public segments: Segment[], - public skips = 0, - ) { - this.startNode = getStarterNode(type, segments); - this.length = calculateCandidateMatchLength(segments); - } + for (const candidate of aSideCandidates) { + const newCandidate = getCandidateMatch(candidate, nodeB); - static createAddition(node: Node) { - assert(node.side === Side.b, () => "Trying to create a deletion but received an A side node"); - return new Change(DiffType.addition, [ - [ - // Start A - -1, - // Start B - node.index, - // Length - 1, - ], - ]); + if (isLatterCandidateBetter(bestMatch, newCandidate)) { + bestMatch = newCandidate; + } } - static createDeletion(node: Node) { - assert(node.side === Side.a, () => "Trying to create a deletion but received an B side node"); - return new Change(DiffType.deletion, [ - [ - // Start A - node.index, - // Start B - -1, - // Length - 1, - ], - ]); - } + return bestMatch; +} - static createMove(match: CandidateMatch) { - return new Change(DiffType.move, match.segments, match.skips); +export function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { + const nodesInMatch = getAllNodesFromMatch(match); + + const allNodes = new Set(); + + for (const node of nodesInMatch) { + const similarNodes = _context.iterB.getMatchingNodes(node); + + for (const _node of similarNodes) { + allNodes.add(_node); + } } + + allNodes.delete(starterNode); + + return [...allNodes]; } // SCORE FN PARAMETERS @@ -95,7 +77,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { // If one of the iterators ends then there is no more search to do if (!nextA || !nextB) { - assert(segmentLength > 0, () => "Segment length is 0") + assert(segmentLength > 0, () => "Segment length is 0"); segments.push([ currentASegmentStart, @@ -112,7 +94,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { continue; } - assert(segmentLength > 0, () => "Segment length is 0") + assert(segmentLength > 0, () => "Segment length is 0"); // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); @@ -239,7 +221,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { skipsInLookaheadB++; } - break mainLoop + break mainLoop; } return { @@ -248,32 +230,3 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { segments, }; } - -function equals(nodeOne: Node, nodeTwo: Node): boolean { - return nodeOne.kind === nodeTwo.kind && nodeOne.text === nodeTwo.text; -} - -export function calculateCandidateMatchLength(segments: Segment[]) { - let sum = 0; - - for (const segment of segments) { - sum += segment[2]; - } - - assert(sum > 0, () => "Segment length is 0") - - return sum; -} - -function getStarterNode(type: DiffType, segments: Segment[]) { - const { a, b } = getIndexesFromSegment(segments[0]); - - const iter = type & TypeMasks.AddOrMove ? _context.iterB : _context.iterA; - const index = type & TypeMasks.AddOrMove ? b.start : a.start; - - const node = iter.nodes[index]; - - assert(node, () => "Failed to get starter node"); - - return node; -} diff --git a/src/v2/index.ts b/src/v2/index.ts index c585633..70314ed 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,16 +1,15 @@ -import { Context, getIndexesFromSegment } from "./utils"; +import { Context } from "./utils"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; -import { getBestMatch, getSubSequenceNodes, isLatterCandidateBetter, oneSidedIteration } from "./core"; -import { Change } from "./diff"; -import { DiffType } from "../types"; +import { computeDiff } from "./core"; import { asciiRenderFn, fail, prettyRenderFn } from "../debug"; -import { range } from "../utils"; import { Options, OutputType, ResultTypeMapper } from "./types"; import { applyChangesToSources } from "./printer"; +import { Change } from "./change"; -const defaultOptions: Options = { +const defaultOptions: Required = { outputType: OutputType.changes, + tryAlignMoves: true, }; export function getDiff2<_OutputType extends OutputType = OutputType.changes>(sourceA: string, sourceB: string, options?: Options<_OutputType>): ResultTypeMapper[_OutputType] { @@ -19,81 +18,11 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so const iterA = new Iterator(sourceA, Side.a); const iterB = new Iterator(sourceB, Side.b); - // console.log('------------- A ----------') - // iterA.nodes.map((x, i) => { - // console.log(i, "|", x.start, x.end, x.prettyKind, `"${x.text}"`) - // }) + let changes: Change[] = []; - // console.log('------------- B ----------') - // iterB.nodes.map((x, i) => { - // console.log(i, "|", x.start, x.end, x.prettyKind, `"${x.text}"`) - // }) + _context = new Context(sourceA, sourceB, iterA, iterB, changes); - const changes: Change[] = []; - - _context = new Context(sourceA, sourceB, changes); - - _context.iterA = iterA; - _context.iterB = iterB; - - while (true) { - const a = iterA.next(); - const b = iterB.next(); - - // No more nodes to process. We are done - if (!a && !b) { - break; - } - - // One of the iterators ran out of nodes. We will mark the remaining unmatched nodes - if (!a || !b) { - // If A finished means that B still have nodes, they are additions. If B finished means that A have nodes, they are deletions - const iterOn = !a ? iterB : iterA; - const type = !a ? DiffType.addition : DiffType.deletion; - const startFrom = !a ? b?.index : a.index; - - const remainingChanges = oneSidedIteration(iterOn, type, startFrom!); - changes.push(...remainingChanges); - break; - } - - // 1- - const bestMatchForB = getBestMatch(b); - - if (!bestMatchForB) { - const addition = Change.createAddition(b); - changes.push(addition); - iterB.mark(b.index, DiffType.addition); - - continue; - } - - // May be empty if the node we are looking for was the only one - const subSequenceNodesToCheck = getSubSequenceNodes(bestMatchForB, b); - - let bestCandidate = bestMatchForB; - - for (const node of subSequenceNodesToCheck) { - const newCandidate = getBestMatch(node); - - if (!newCandidate) { - const addition = Change.createAddition(b); - changes.push(addition); - iterB.mark(b.index, DiffType.addition); - - continue; - } - - if (isLatterCandidateBetter(bestCandidate, newCandidate)) { - bestCandidate = newCandidate; - } - } - - const move = Change.createMove(bestCandidate); - changes.push(move); - markMatched(move); - continue; - } + changes = computeDiff(); switch (_options.outputType) { case OutputType.changes: { @@ -110,18 +39,4 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so } } -function markMatched(change: Change) { - for (const segment of change.segments) { - const { a, b } = getIndexesFromSegment(segment); - - for (const index of range(a.start, a.end)) { - _context.iterA.mark(index, change.type); - } - - for (const index of range(b.start, b.end)) { - _context.iterB.mark(index, change.type); - } - } -} - export let _context: Context; diff --git a/src/v2/printer.ts b/src/v2/printer.ts index e7c03c9..f2ff2d2 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -3,11 +3,11 @@ import colorFn from "kleur"; import { asciiRenderFn, assert, createTextTable, fail, prettyRenderFn, RenderFn } from "../debug"; import { DiffType, TypeMasks } from "../types"; -import { Change } from "./diff"; import { capitalizeFirstLetter, getIndexesFromSegment } from "./utils"; import { Iterator } from "./iterator"; import { _context } from "."; import { rangeEq } from "../utils"; +import { Change } from "./change"; export function applyChangesToSources( sourceA: string, diff --git a/src/v2/types.ts b/src/v2/types.ts index cb031f7..9d0aeea 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -1,5 +1,4 @@ -import { DiffType } from "../types"; -import { Change } from "./diff"; +import { Change } from "./change"; import { Node } from "./node"; export type SyntaxKind = number; @@ -13,7 +12,8 @@ export interface ParsedProgram { } export interface Options<_OutputType extends OutputType = OutputType.changes> { - outputType: _OutputType; + outputType?: _OutputType; + tryAlignMoves?: boolean; } export enum OutputType { @@ -27,3 +27,12 @@ export interface ResultTypeMapper { [OutputType.text]: { sourceA: string; sourceB: string }; [OutputType.prettyText]: { sourceA: string; sourceB: string }; } + +// Start is inclusive, end is not inclusive +export type Segment = [indexA: number, indexB: number, length: number]; + +export interface CandidateMatch { + length: number; + skips: number; + segments: Segment[]; +} diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 995e315..7eb4d5a 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -2,22 +2,66 @@ import { _context } from "."; import { assert } from "../debug"; import { Side } from "../shared/language"; import { range } from "../utils"; -import { CandidateMatch, Change, Segment } from "./diff"; +import { Change } from "./change"; import { Iterator } from "./iterator"; import { Node } from "./node"; +import { CandidateMatch, Segment } from "./types"; export class Context { // Iterators will get stored once they are initialize, which happens later on the execution - iterA!: Iterator; - iterB!: Iterator; constructor( public sourceA: string, public sourceB: string, - public changes: Change[] + public iterA: Iterator, + public iterB: Iterator, + public changes: Change[], ) {} } +export function equals(nodeOne: Node, nodeTwo: Node): boolean { + return nodeOne.kind === nodeTwo.kind && nodeOne.text === nodeTwo.text; +} + +/** + * TODO: MAybe compute a score fn + */ +export function isLatterCandidateBetter(currentCandidate: CandidateMatch, newCandidate: CandidateMatch): boolean { + if (newCandidate.length > currentCandidate.length) { + return true; + } else if (newCandidate.length < currentCandidate.length) { + return false; + } + + if (newCandidate.segments.length < currentCandidate.segments.length) { + return true; + } else if (newCandidate.segments.length < currentCandidate.segments.length) { + return false; + } + + return newCandidate.skips < currentCandidate.skips; +} + +export function calculateCandidateMatchLength(segments: Segment[]) { + let sum = 0; + + for (const segment of segments) { + sum += segment[2]; + } + + assert(sum > 0, () => "Segment length is 0"); + + return sum; +} + +export function getEmptyCandidate(): CandidateMatch { + return { + length: 0, + segments: [], + skips: 0, + }; +} + export function getAllNodesFromMatch(match: CandidateMatch, side = Side.b) { const iter = getIterFromSide(side); const readFrom = side === Side.a ? 0 : 1; From d5164532c83d60eecfdd908bf53526d04d4fb450 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sun, 17 Mar 2024 14:42:41 -0300 Subject: [PATCH 26/79] wip --- src/v2/semanticAligment.ts | 134 +++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/v2/semanticAligment.ts diff --git a/src/v2/semanticAligment.ts b/src/v2/semanticAligment.ts new file mode 100644 index 0000000..82acbee --- /dev/null +++ b/src/v2/semanticAligment.ts @@ -0,0 +1,134 @@ +import { applyAlignments } from "../backend/printer"; +import { Side } from "../shared/language"; +import { DiffType } from "../types"; +import { rangeEq } from "../utils"; +import { Change } from "./change"; +import { Segment } from "./types"; +import { getIndexesFromSegment } from "./utils"; + +export interface Offset { + index: number; + type: DiffType; + triggeringChange?: Change; +} + +// number = index +export type OffsetsMap = Map; + +const alignmentTable = { + a: new Map() as OffsetsMap, + b: new Map() as OffsetsMap, +}; + +export function computeMoveAlignment(changes: Change[]): Change[] { + const resultChanges: Change[] = []; + + for (const change of changes) { + let canMoveBeAligned = true; + segmentLoop: for (const segment of change.segments) { + if (!canSegmentBeAligned(segment)) { + canMoveBeAligned = false; + break segmentLoop; + } + } + + if (canMoveBeAligned) { + const { side, offset } = getAlignment() + addAlignment(side, offset); + } else { + resultChanges.push(change); + } + } + + return resultChanges; +} + +function getAlignment(change: Change): { side: Side, offset: Offset } { + for (const segment of change.segments) { + const + } +} + +function addAlignment(side: Side, offset: Offset) { + alignmentTable[side].set(offset.index, offset) +} + +function canSegmentBeAligned(segment: Segment): { canBeAligned: boolean, offsets?: Offset[] } { + const { a, b } = getIndexesFromSegment(segment); + + const offsettedAIndex = getOffsettedIndex(Side.a, a.start); + const offsettedBIndex = getOffsettedIndex(Side.b, b.start); + + if (offsettedAIndex === offsettedBIndex) { + return { canBeAligned: true }; + } + + const sideToIterate = offsettedAIndex < offsettedBIndex ? Side.a : Side.b; + const offsetsToCheck = alignmentTable[sideToIterate]; + const startIndex = sideToIterate === Side.a ? offsettedAIndex : offsettedBIndex; + const indexDiff = Math.abs(offsettedAIndex - offsettedBIndex); + + const offsets: Offset[] = [] + + let canBeAligned = true; + for (const i of rangeEq(startIndex, startIndex + indexDiff)) { + const offset = offsetsToCheck.get(i); + + // The offset trackers contains allegement from all the change types, but when it comes to movement alignment + // we are only interested in the ones coming from`moves` + if (!offset || offset.type !== DiffType.move) { + continue; + } + + offsets.push({ + index: i, + type: DiffType.move, + }) + + canBeAligned = false + break; + } + + return { + canBeAligned, + offsets + } +} + +function getOffsettedIndex(side: Side, targetIndex: number) { + const ogIndex = targetIndex; + const _side = alignmentTable[side] + + let offset = 0; + // TODO: Use and array with insertion sort + // The offset is unsorted, so we need to order the indexes first before processing it + for (const index of [..._side.keys()].sort((a, b) => a > b ? 1 : -1)) { + if (index <= targetIndex) { + // We increase the target index so that if we are inside an alignment (example bellow) we can read the offsets properly, for example: + // + // A B + // ------------ + // 1 1 + // 2 2 + // 3 1 + // 2 + // 3 + // + // A B + // ------------ + // - 1 + // - 2 + // 1 1 + // 2 2 + // 3 3 + // + // Alignments are [0, 1] in "offsetA", the "1" in A side has index 0, so if we don't do anything special only the first alignment will be included + targetIndex++; + offset++; + } else { + break; + } + } + + return ogIndex + offset; +} \ No newline at end of file From f243c6a4c3a5d2b762acea46a066e6dae2e126f4 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 18 Mar 2024 18:20:37 -0300 Subject: [PATCH 27/79] Working --- src/v2/semanticAligment.ts | 94 ++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/src/v2/semanticAligment.ts b/src/v2/semanticAligment.ts index 82acbee..cfc94ac 100644 --- a/src/v2/semanticAligment.ts +++ b/src/v2/semanticAligment.ts @@ -1,10 +1,10 @@ import { applyAlignments } from "../backend/printer"; import { Side } from "../shared/language"; import { DiffType } from "../types"; -import { rangeEq } from "../utils"; +import { range, rangeEq } from "../utils"; import { Change } from "./change"; import { Segment } from "./types"; -import { getIndexesFromSegment } from "./utils"; +import { calculateCandidateMatchLength, getIndexesFromSegment } from "./utils"; export interface Offset { index: number; @@ -21,56 +21,80 @@ const alignmentTable = { }; export function computeMoveAlignment(changes: Change[]): Change[] { - const resultChanges: Change[] = []; + const unalignedChanges: Change[] = []; for (const change of changes) { - let canMoveBeAligned = true; - segmentLoop: for (const segment of change.segments) { - if (!canSegmentBeAligned(segment)) { - canMoveBeAligned = false; - break segmentLoop; + const unalignedSegments: Segment[] = []; + + for (const segment of change.segments) { + if (canSegmentBeAligned(segment)) { + addAlignment(segment); + } else { + unalignedSegments.push(segment); } } - if (canMoveBeAligned) { - const { side, offset } = getAlignment() - addAlignment(side, offset); + if (unalignedSegments.length === 0) { + // Fully aligned + continue } else { - resultChanges.push(change); + if (unalignedSegments.length !== change.length) { + change.segments = unalignedSegments; + change.length = calculateCandidateMatchLength(unalignedSegments); + } + + unalignedChanges.push(change); } } - return resultChanges; + return unalignedChanges; } -function getAlignment(change: Change): { side: Side, offset: Offset } { - for (const segment of change.segments) { - const +function addAlignment(segment: Segment) { + const { a, b } = getIndexesFromSegment(segment); + + const offsettedA = { start: getOffsettedIndex(Side.a, a.start), end: getOffsettedIndex(Side.a, a.end) }; + const offsettedB = { start: getOffsettedIndex(Side.b, b.start), end: getOffsettedIndex(Side.b, b.end) }; + + const indexDiff = Math.abs(offsettedA.start - offsettedB.start); + + const sideToAlignStart = offsettedA.start < offsettedB.start ? Side.a : Side.b; + + function apply(side: Side, index: number) { + for (const i of range(index, index + indexDiff)) { + alignmentTable[side].set(i, { index: i, type: DiffType.move }); + } } -} -function addAlignment(side: Side, offset: Offset) { - alignmentTable[side].set(offset.index, offset) + if (sideToAlignStart === Side.a) { + apply(Side.a, offsettedA.start); + apply(Side.b, offsettedB.end); + } else { + apply(Side.a, offsettedA.end); + apply(Side.b, offsettedB.start); + } } -function canSegmentBeAligned(segment: Segment): { canBeAligned: boolean, offsets?: Offset[] } { +type SegmentAlignmentCheckResult = + | { canBeAligned: false } + | { canBeAligned: true; offsets: Offset[]; sideToAlign: Side }; + +function canSegmentBeAligned(segment: Segment): boolean { const { a, b } = getIndexesFromSegment(segment); const offsettedAIndex = getOffsettedIndex(Side.a, a.start); const offsettedBIndex = getOffsettedIndex(Side.b, b.start); if (offsettedAIndex === offsettedBIndex) { - return { canBeAligned: true }; + return true } const sideToIterate = offsettedAIndex < offsettedBIndex ? Side.a : Side.b; const offsetsToCheck = alignmentTable[sideToIterate]; - const startIndex = sideToIterate === Side.a ? offsettedAIndex : offsettedBIndex; + const startIndex = + sideToIterate === Side.a ? offsettedAIndex : offsettedBIndex; const indexDiff = Math.abs(offsettedAIndex - offsettedBIndex); - const offsets: Offset[] = [] - - let canBeAligned = true; for (const i of rangeEq(startIndex, startIndex + indexDiff)) { const offset = offsetsToCheck.get(i); @@ -80,29 +104,21 @@ function canSegmentBeAligned(segment: Segment): { canBeAligned: boolean, offsets continue; } - offsets.push({ - index: i, - type: DiffType.move, - }) - - canBeAligned = false - break; + + return false; } - return { - canBeAligned, - offsets - } + return true; } function getOffsettedIndex(side: Side, targetIndex: number) { const ogIndex = targetIndex; - const _side = alignmentTable[side] + const _side = alignmentTable[side]; let offset = 0; // TODO: Use and array with insertion sort // The offset is unsorted, so we need to order the indexes first before processing it - for (const index of [..._side.keys()].sort((a, b) => a > b ? 1 : -1)) { + for (const index of [..._side.keys()].sort((a, b) => (a > b ? 1 : -1))) { if (index <= targetIndex) { // We increase the target index so that if we are inside an alignment (example bellow) we can read the offsets properly, for example: // @@ -131,4 +147,4 @@ function getOffsettedIndex(side: Side, targetIndex: number) { } return ogIndex + offset; -} \ No newline at end of file +} From 515cb63faccf5a501781a91ea84b94038fc94412 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 18 Mar 2024 18:20:44 -0300 Subject: [PATCH 28/79] x --- src/v2/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/v2/index.ts b/src/v2/index.ts index 70314ed..9210710 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -6,6 +6,7 @@ import { asciiRenderFn, fail, prettyRenderFn } from "../debug"; import { Options, OutputType, ResultTypeMapper } from "./types"; import { applyChangesToSources } from "./printer"; import { Change } from "./change"; +import { computeMoveAlignment } from "./semanticAligment"; const defaultOptions: Required = { outputType: OutputType.changes, @@ -24,6 +25,10 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so changes = computeDiff(); + if (_options.tryAlignMoves) { + changes = computeMoveAlignment(changes) + } + switch (_options.outputType) { case OutputType.changes: { return changes as ResultTypeMapper[_OutputType]; From efb5c681d3621c1ed61d96153e4ad0f8abaebc6e Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 20 Mar 2024 12:19:11 -0300 Subject: [PATCH 29/79] Only align moves --- src/v2/semanticAligment.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/v2/semanticAligment.ts b/src/v2/semanticAligment.ts index cfc94ac..44f3d74 100644 --- a/src/v2/semanticAligment.ts +++ b/src/v2/semanticAligment.ts @@ -24,6 +24,11 @@ export function computeMoveAlignment(changes: Change[]): Change[] { const unalignedChanges: Change[] = []; for (const change of changes) { + if (change.type !== DiffType.move) { + unalignedChanges.push(change); + continue + } + const unalignedSegments: Segment[] = []; for (const segment of change.segments) { From 6161d6832e218458bfd2d3b82cad85a7425a51f8 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 20 Mar 2024 15:52:01 -0300 Subject: [PATCH 30/79] wip compaction --- src/v2/compact.ts | 235 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 src/v2/compact.ts diff --git a/src/v2/compact.ts b/src/v2/compact.ts new file mode 100644 index 0000000..0790255 --- /dev/null +++ b/src/v2/compact.ts @@ -0,0 +1,235 @@ +// import { fail } from "../debug"; +// import { DiffType, TypeMasks } from "../types"; +// import { Change } from "./change"; +// import { Segment } from "./types"; +// import { getIndexesFromSegment } from "./utils"; + +import { assert, fail } from "../debug"; +import { DiffType } from "../types"; +import { Change } from "./change"; +import { Segment } from "./types"; +import { getIndexesFromSegment } from "./utils"; + +// export function compactChanges(changes: Change[]): Change[] { +// const moves: Change[] = []; +// const changesToCompact: Change[] = []; + +// for (const change of changes) { +// if (change.type !== DiffType.move) { +// moves.push(change); +// continue; +// } + +// changesToCompact.push(change); +// } + +// const compactedChanges: Change[] = []; +// for (let index = 0; index < changesToCompact.length - 1; index++) { +// const current = changesToCompact[index]; +// const next = changesToCompact[index + 1]; + +// if (!next) { +// fail('OOOPPSPSPS') +// compactedChanges.push(current); +// break; +// } + +// const currentSegments = current.segments; +// const nextSegments = next.segments; + +// // For each segment ... + +// // ASD +// // if (!areIndexesCompatible(currentSegments, nextSegments)) { +// // compactedChanges.push(current); +// // continue; +// // } + +// // We have a compatibility, start the inner loop to see if there are more compatible nodes ahead + +// let innerCursor = index + 1; + +// // Values to accumulate +// const indexes = []// A[...currentIndexes, ...nextIndexes]; // Skip first two entries since we know they are compatible + +// innerLoop: while (true) { +// const _current = changesToCompact[innerCursor]; +// const _next = changesToCompact[innerCursor + 1]; + +// if (!_next) { +// break innerLoop; +// } + +// const _indexesCurrent = _current[indexesProp]; +// const _indexesNext = _next[indexesProp]; + +// if (!areIndexesCompatible(_indexesCurrent, _indexesNext)) { +// break innerLoop; +// } + +// indexes.push(..._indexesNext); + +// // TODO: Enable this? Find a test case first +// // closingNodeIndexes.push(..._current.indexesOfClosingMoves, ..._next.indexesOfClosingMoves) + +// innerCursor++; +// } + +// const newChange = new Diff(type, indexes); + +// // TODO-NOW replace indexesOfClosingMoves with ids +// //newChange.indexesOfClosingMoves + +// compactedChanges.push(newChange); + +// index = innerCursor; +// } + +// return [...compactedChanges, ...moves]; +// } + +// function areIndexesCompatible(segmentOne: Segment, segmentTwo: Segment) { +// const one = getIndexesFromSegment(segmentOne); +// const two = getIndexesFromSegment(segmentTwo); + +// return one.a.end + 1 === two.a.start && one.b.end + 1=== two.b.start; +// } + +// This function takes an array of numbers and returns another array of numbers. if the number are sequential, such as 1, 2, 3 it merges them together into one segment, otherwise it pushes the unmergable number into the result array and continues with other nubers +function mergeNumbers(numbers: number[]): number[] { + const result: number[] = []; + + for (let index = 0; index < numbers.length - 1; index++) { + const current = numbers[index]; + const next = numbers[index + 1]; + + if (current + 1 === next) { + result.push(current); + } else { + result.push(current); + result.push(next); + } + } + + return result; +} + +export function compactAndCreateDiff( + diffType: DiffType.addition | DiffType.deletion, + segments: Segment[] +) { + const finalSegments: Segment[] = []; + + // The segment format is [indexA, indexB, length]. Additions happen on B, deletions on A + const indexToCheck = diffType === DiffType.addition ? 1 : 0; + + let i = 0; + let segmentStart = segments[i][indexToCheck]; + let currentSegmentLength = 1; + + while (true) { + const currentSegment = segments[i]; + const nextSegment = segments[i + 1]; + + i++; + + // No more segments to check, push the last segment before breaking the loop + if (!nextSegment) { + finalSegments.push(currentSegment); + break; + } + + assert( + currentSegment[2] === 1 && nextSegment[2] === 1, + () => "Additions and deletions must be of length 1" + ); + + // Since additions and deletions are always of length 1 the only thing we need to check is if the indexes are consecutive + const first = currentSegment[indexToCheck] + 1; + const second = nextSegment[indexToCheck]; + + if (first === second) { + // Consecutive indexes found, start the compaction + currentSegmentLength++; + } else { + // Compaction broke, push the current segment and start a new one + + let newSegment: Segment; + if (diffType === DiffType.addition) { + newSegment = [ + // Index A + segmentStart, + // Index B + -1, + // Length + currentSegmentLength, + ]; + } else { + newSegment = [ + // Index A + -1, + // Index B + segmentStart, + // Length + currentSegmentLength, + ]; + } + + finalSegments.push(newSegment); + + // Restart values for a new compaction + currentSegmentLength = 1; + segmentStart = currentSegment[indexToCheck]; + } + } + + if (diffType === DiffType.addition) { + return Change.createAddition(finalSegments); + } else { + return Change.createDeletion(finalSegments); + } +} + +// export function compactAndCreateDiffOLD( +// diffType: DiffType.addition | DiffType.deletion, +// segments: Segment[] +// ) { +// const resultingSegments: Segment[] = []; + +// for (let index = 0; index < segments.length - 1; index++) { +// const currentEnd = segments[index][1]; +// const nextStart = segments[index + 1][0]; + +// if (!nextStart) { +// fail("OOOPPSPSPS"); +// // resultingSegments.push(current); +// break; +// } + +// if (currentEnd + 1 === nextStart) { +// console.log("compacted", currentEnd, nextStart); +// } else { +// resultingSegments.push(segments[index]); +// } +// } + +// if (diffType === DiffType.addition) { +// return Change.createAddition(resultingSegments); +// } else { +// return Change.createDeletion(resultingSegments); +// } +// } + +// function areSegmentsContactable2(segmentOne: Segment, segmentTwo: Segment) { +// const one = getIndexesFromSegment(segmentOne); +// const two = getIndexesFromSegment(segmentTwo); + +// return one.a.end + 1 === two.a.start && one.b.end + 1 === two.b.start; +// } + +// function areSegmentsContactable(firstIndex: number, segmentTwo: Segment) { +// const one = getIndexesFromSegment(segmentOne); +// const two = getIndexesFromSegment(segmentTwo); + +// return one.a.end + 1 === two.a.start && one.b.end + 1 === two.b.start; +// } From c603ee1ec5780a098ecb44875fca517dff843f5c Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 20 Mar 2024 17:57:20 -0300 Subject: [PATCH 31/79] Addition and removals compaction --- src/v2/change.ts | 30 ++---- src/v2/compact.ts | 214 +++---------------------------------- src/v2/core.ts | 26 ++--- src/v2/index.ts | 23 +++- src/v2/node.ts | 36 ++++++- src/v2/semanticAligment.ts | 12 +-- src/v2/utils.ts | 2 + 7 files changed, 91 insertions(+), 252 deletions(-) diff --git a/src/v2/change.ts b/src/v2/change.ts index bac271f..bb1dd12 100644 --- a/src/v2/change.ts +++ b/src/v2/change.ts @@ -18,32 +18,14 @@ export class Change { this.length = calculateCandidateMatchLength(segments); } - static createAddition(node: Node) { - assert(node.side === Side.b, () => "Trying to create a deletion but received an A side node"); - return new Change(DiffType.addition, [ - [ - // Start A - -1, - // Start B - node.index, - // Length - 1, - ], - ]); + static createAddition(segments: Segment[]) { + assert(segments.length !== 0, () => "No segments received"); + return new Change(DiffType.addition, segments); } - static createDeletion(node: Node) { - assert(node.side === Side.a, () => "Trying to create a deletion but received an B side node"); - return new Change(DiffType.deletion, [ - [ - // Start A - node.index, - // Start B - -1, - // Length - 1, - ], - ]); + static createDeletion(segments: Segment[]) { + assert(segments.length !== 0, () => "No segments received"); + return new Change(DiffType.deletion, segments); } static createMove(match: CandidateMatch) { diff --git a/src/v2/compact.ts b/src/v2/compact.ts index 0790255..3fc1481 100644 --- a/src/v2/compact.ts +++ b/src/v2/compact.ts @@ -1,122 +1,11 @@ -// import { fail } from "../debug"; -// import { DiffType, TypeMasks } from "../types"; -// import { Change } from "./change"; -// import { Segment } from "./types"; -// import { getIndexesFromSegment } from "./utils"; - -import { assert, fail } from "../debug"; +import { assert } from "../debug"; import { DiffType } from "../types"; import { Change } from "./change"; import { Segment } from "./types"; -import { getIndexesFromSegment } from "./utils"; - -// export function compactChanges(changes: Change[]): Change[] { -// const moves: Change[] = []; -// const changesToCompact: Change[] = []; - -// for (const change of changes) { -// if (change.type !== DiffType.move) { -// moves.push(change); -// continue; -// } - -// changesToCompact.push(change); -// } - -// const compactedChanges: Change[] = []; -// for (let index = 0; index < changesToCompact.length - 1; index++) { -// const current = changesToCompact[index]; -// const next = changesToCompact[index + 1]; - -// if (!next) { -// fail('OOOPPSPSPS') -// compactedChanges.push(current); -// break; -// } - -// const currentSegments = current.segments; -// const nextSegments = next.segments; - -// // For each segment ... - -// // ASD -// // if (!areIndexesCompatible(currentSegments, nextSegments)) { -// // compactedChanges.push(current); -// // continue; -// // } - -// // We have a compatibility, start the inner loop to see if there are more compatible nodes ahead - -// let innerCursor = index + 1; - -// // Values to accumulate -// const indexes = []// A[...currentIndexes, ...nextIndexes]; // Skip first two entries since we know they are compatible - -// innerLoop: while (true) { -// const _current = changesToCompact[innerCursor]; -// const _next = changesToCompact[innerCursor + 1]; - -// if (!_next) { -// break innerLoop; -// } - -// const _indexesCurrent = _current[indexesProp]; -// const _indexesNext = _next[indexesProp]; - -// if (!areIndexesCompatible(_indexesCurrent, _indexesNext)) { -// break innerLoop; -// } - -// indexes.push(..._indexesNext); - -// // TODO: Enable this? Find a test case first -// // closingNodeIndexes.push(..._current.indexesOfClosingMoves, ..._next.indexesOfClosingMoves) - -// innerCursor++; -// } - -// const newChange = new Diff(type, indexes); - -// // TODO-NOW replace indexesOfClosingMoves with ids -// //newChange.indexesOfClosingMoves - -// compactedChanges.push(newChange); - -// index = innerCursor; -// } - -// return [...compactedChanges, ...moves]; -// } - -// function areIndexesCompatible(segmentOne: Segment, segmentTwo: Segment) { -// const one = getIndexesFromSegment(segmentOne); -// const two = getIndexesFromSegment(segmentTwo); - -// return one.a.end + 1 === two.a.start && one.b.end + 1=== two.b.start; -// } - -// This function takes an array of numbers and returns another array of numbers. if the number are sequential, such as 1, 2, 3 it merges them together into one segment, otherwise it pushes the unmergable number into the result array and continues with other nubers -function mergeNumbers(numbers: number[]): number[] { - const result: number[] = []; - - for (let index = 0; index < numbers.length - 1; index++) { - const current = numbers[index]; - const next = numbers[index + 1]; - - if (current + 1 === next) { - result.push(current); - } else { - result.push(current); - result.push(next); - } - } - - return result; -} export function compactAndCreateDiff( diffType: DiffType.addition | DiffType.deletion, - segments: Segment[] + segments: Segment[], ) { const finalSegments: Segment[] = []; @@ -124,11 +13,12 @@ export function compactAndCreateDiff( const indexToCheck = diffType === DiffType.addition ? 1 : 0; let i = 0; - let segmentStart = segments[i][indexToCheck]; - let currentSegmentLength = 1; + let currentSegment = segments[i]; + let currentStart = currentSegment[indexToCheck]; + + assert(currentSegment[2] === 1, () => "At this point additions and deletions must be of length 1"); while (true) { - const currentSegment = segments[i]; const nextSegment = segments[i + 1]; i++; @@ -139,47 +29,19 @@ export function compactAndCreateDiff( break; } - assert( - currentSegment[2] === 1 && nextSegment[2] === 1, - () => "Additions and deletions must be of length 1" - ); - + assert(nextSegment[2] === 1, () => "Additions and deletions must be of length 1"); // Since additions and deletions are always of length 1 the only thing we need to check is if the indexes are consecutive - const first = currentSegment[indexToCheck] + 1; - const second = nextSegment[indexToCheck]; - - if (first === second) { - // Consecutive indexes found, start the compaction - currentSegmentLength++; - } else { - // Compaction broke, push the current segment and start a new one - let newSegment: Segment; - if (diffType === DiffType.addition) { - newSegment = [ - // Index A - segmentStart, - // Index B - -1, - // Length - currentSegmentLength, - ]; - } else { - newSegment = [ - // Index A - -1, - // Index B - segmentStart, - // Length - currentSegmentLength, - ]; - } + const next = nextSegment[indexToCheck]; - finalSegments.push(newSegment); - - // Restart values for a new compaction - currentSegmentLength = 1; - segmentStart = currentSegment[indexToCheck]; + if (currentStart + 1 === next) { + // Valid compaction case, increase the length of the segment + currentSegment[2]++; + currentStart = next; + } else { + // Compaction broke, push current segment and reset values for the next one + finalSegments.push(currentSegment); + currentSegment = nextSegment; } } @@ -189,47 +51,3 @@ export function compactAndCreateDiff( return Change.createDeletion(finalSegments); } } - -// export function compactAndCreateDiffOLD( -// diffType: DiffType.addition | DiffType.deletion, -// segments: Segment[] -// ) { -// const resultingSegments: Segment[] = []; - -// for (let index = 0; index < segments.length - 1; index++) { -// const currentEnd = segments[index][1]; -// const nextStart = segments[index + 1][0]; - -// if (!nextStart) { -// fail("OOOPPSPSPS"); -// // resultingSegments.push(current); -// break; -// } - -// if (currentEnd + 1 === nextStart) { -// console.log("compacted", currentEnd, nextStart); -// } else { -// resultingSegments.push(segments[index]); -// } -// } - -// if (diffType === DiffType.addition) { -// return Change.createAddition(resultingSegments); -// } else { -// return Change.createDeletion(resultingSegments); -// } -// } - -// function areSegmentsContactable2(segmentOne: Segment, segmentTwo: Segment) { -// const one = getIndexesFromSegment(segmentOne); -// const two = getIndexesFromSegment(segmentTwo); - -// return one.a.end + 1 === two.a.start && one.b.end + 1 === two.b.start; -// } - -// function areSegmentsContactable(firstIndex: number, segmentTwo: Segment) { -// const one = getIndexesFromSegment(segmentOne); -// const two = getIndexesFromSegment(segmentTwo); - -// return one.a.end + 1 === two.a.start && one.b.end + 1 === two.b.start; -// } diff --git a/src/v2/core.ts b/src/v2/core.ts index d133b2d..a09f0d0 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -7,7 +7,7 @@ import { Change } from "./change"; import { range } from "../utils"; export function computeDiff() { - const { iterA, iterB, changes } = _context; + const { iterA, iterB, changes, additions } = _context; while (true) { const a = iterA.next(); @@ -24,8 +24,7 @@ export function computeDiff() { const iterOn = !a ? iterB : iterA; const type = !a ? DiffType.addition : DiffType.deletion; - const remainingChanges = oneSidedIteration(iterOn, type); - changes.push(...remainingChanges); + oneSidedIteration(iterOn, type); break; } @@ -33,8 +32,7 @@ export function computeDiff() { const bestMatchForB = getBestMatch(b); if (!bestMatchForB) { - const addition = Change.createAddition(b); - changes.push(addition); + additions.push(b.getSegment(DiffType.addition)); iterB.mark(b.index, DiffType.addition); continue; @@ -49,8 +47,7 @@ export function computeDiff() { const newCandidate = getBestMatch(node); if (!newCandidate) { - const addition = Change.createAddition(b); - changes.push(addition); + additions.push(b.getSegment(DiffType.addition)); iterB.mark(b.index, DiffType.addition); continue; @@ -87,25 +84,18 @@ function markMatched(change: Change) { export function oneSidedIteration( iter: Iterator, typeOfChange: DiffType.addition | DiffType.deletion, -): Change[] { - const changes: Change[] = []; +) { + const { additions, deletions } = _context; let node = iter.next(); - - // TODO: Compactar? while (node) { - let change: Change; - if (typeOfChange === DiffType.addition) { - change = Change.createAddition(node); + additions.push(node.getSegment(DiffType.addition)); } else { - change = Change.createDeletion(node); + deletions.push(node.getSegment(DiffType.deletion)); } - changes.push(change); iter.mark(node.index, typeOfChange); node = iter.next(node.index + 1); } - - return changes; } diff --git a/src/v2/index.ts b/src/v2/index.ts index 9210710..f35a5ca 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -3,10 +3,12 @@ import { Side } from "../shared/language"; import { Iterator } from "./iterator"; import { computeDiff } from "./core"; import { asciiRenderFn, fail, prettyRenderFn } from "../debug"; -import { Options, OutputType, ResultTypeMapper } from "./types"; +import { Options, OutputType, ResultTypeMapper, Segment } from "./types"; import { applyChangesToSources } from "./printer"; import { Change } from "./change"; import { computeMoveAlignment } from "./semanticAligment"; +import { compactAndCreateDiff } from "./compact"; +import { DiffType } from "../types"; const defaultOptions: Required = { outputType: OutputType.changes, @@ -21,12 +23,27 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so let changes: Change[] = []; - _context = new Context(sourceA, sourceB, iterA, iterB, changes); + const deletions: Segment[] = []; + const additions: Segment[] = []; + + _context = new Context(sourceA, sourceB, iterA, iterB, changes, deletions, additions); changes = computeDiff(); if (_options.tryAlignMoves) { - changes = computeMoveAlignment(changes) + changes = computeMoveAlignment(changes); + } + + // We compact all tracked additions and deletions into a single change with multiple segments, we also compact them if possible + + if (_context.additions.length) { + const additionsChange = compactAndCreateDiff(DiffType.addition, additions); + changes.push(additionsChange); + } + + if (_context.deletions.length) { + const deletionsChange = compactAndCreateDiff(DiffType.deletion, deletions); + changes.push(deletionsChange); } switch (_options.outputType) { diff --git a/src/v2/node.ts b/src/v2/node.ts index a6dd879..38abfe0 100644 --- a/src/v2/node.ts +++ b/src/v2/node.ts @@ -1,5 +1,5 @@ import { getPrettyKind } from "../debug"; -import { SyntaxKind } from "./types"; +import { Segment, SyntaxKind } from "./types"; import { DiffType, Range } from "../types"; import { Side } from "../shared/language"; import { _context } from "."; @@ -37,7 +37,17 @@ export class Node { markedAs?: DiffType; constructor(args: NewNodeArgs) { - const { side, index, globalIndex, kind, text, parent, start, end, isTextNode } = args; + const { + side, + index, + globalIndex, + kind, + text, + parent, + start, + end, + isTextNode, + } = args; this.side = side; this.index = index; @@ -59,6 +69,28 @@ export class Node { }; } + getSegment(diffType: DiffType.addition | DiffType.deletion): Segment { + if (diffType === DiffType.addition) { + return [ + // Start A + -1, + // Start B + this.index, + // Length + 1, + ]; + } else { + return [ + // Start A + this.index, + // Start B + -1, + // Length + 1, + ]; + } + } + mark() { this.matched = true; } diff --git a/src/v2/semanticAligment.ts b/src/v2/semanticAligment.ts index 44f3d74..a00c408 100644 --- a/src/v2/semanticAligment.ts +++ b/src/v2/semanticAligment.ts @@ -26,7 +26,7 @@ export function computeMoveAlignment(changes: Change[]): Change[] { for (const change of changes) { if (change.type !== DiffType.move) { unalignedChanges.push(change); - continue + continue; } const unalignedSegments: Segment[] = []; @@ -41,7 +41,7 @@ export function computeMoveAlignment(changes: Change[]): Change[] { if (unalignedSegments.length === 0) { // Fully aligned - continue + continue; } else { if (unalignedSegments.length !== change.length) { change.segments = unalignedSegments; @@ -62,7 +62,7 @@ function addAlignment(segment: Segment) { const offsettedB = { start: getOffsettedIndex(Side.b, b.start), end: getOffsettedIndex(Side.b, b.end) }; const indexDiff = Math.abs(offsettedA.start - offsettedB.start); - + const sideToAlignStart = offsettedA.start < offsettedB.start ? Side.a : Side.b; function apply(side: Side, index: number) { @@ -91,13 +91,12 @@ function canSegmentBeAligned(segment: Segment): boolean { const offsettedBIndex = getOffsettedIndex(Side.b, b.start); if (offsettedAIndex === offsettedBIndex) { - return true + return true; } const sideToIterate = offsettedAIndex < offsettedBIndex ? Side.a : Side.b; const offsetsToCheck = alignmentTable[sideToIterate]; - const startIndex = - sideToIterate === Side.a ? offsettedAIndex : offsettedBIndex; + const startIndex = sideToIterate === Side.a ? offsettedAIndex : offsettedBIndex; const indexDiff = Math.abs(offsettedAIndex - offsettedBIndex); for (const i of rangeEq(startIndex, startIndex + indexDiff)) { @@ -109,7 +108,6 @@ function canSegmentBeAligned(segment: Segment): boolean { continue; } - return false; } diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 7eb4d5a..7719d05 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -16,6 +16,8 @@ export class Context { public iterA: Iterator, public iterB: Iterator, public changes: Change[], + public deletions: Segment[], + public additions: Segment[], ) {} } From d8f3d9bdc63ee0eb8fcf2fa56e25188ea8d8788e Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 20 Mar 2024 17:57:52 -0300 Subject: [PATCH 32/79] Fix printer when diff crosses multiple lines --- src/v2/printer.ts | 53 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/src/v2/printer.ts b/src/v2/printer.ts index f2ff2d2..c04cd4a 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -136,22 +136,43 @@ export function getSourceWithChange( const compliment = getComplimentArray(charsToAdd); - // Since we might display the changes in a table, where we split by newlines, simply coloring the whole segment won't work, for example: - // - // (COLOR_START) a - // b (COLOR END) - // - // When splitted you will get ["(COLOR_START) a", " b (COLOR END)"] where only the first line would be colored, not the second - // So to fix it we wrap all the tokens individually - // - // (COLOR_START) a (COLOR END) - // (COLOR_START) b (COLOR END) - // - // So that when splitted you get ["(COLOR_START) a (COLOR END)", "(COLOR_START) b (COLOR END)"] which produces the desired output - text = text - .split(" ") - .map((x) => colorFn(x)) - .join(" "); + if (text.includes("\n")) { + // One edge case we need to be aware of is that the text highlighted may be across newlines so simply coloring the whole segment won't work, for example: + // + // (COLOR_START) a + // b (COLOR END) + // + // Later on the line the printer breaks down this code into newlines, so you get ["(COLOR_START) a", " b (COLOR END)"] where only the first line would be colored, not the second + // So to fix it we check if the highlighted text crosses multiple lines, if so we wrap all the tokens individually: + // + // (COLOR_START) a (COLOR END) + // (COLOR_START) b (COLOR END) + // + // So that when splitted you get ["(COLOR_START) a (COLOR END)", "(COLOR_START) b (COLOR END)"] which produces the desired output + + // Here we will perform the above mentioned by: + // - Iterate for each line of code (separated by new lines) + // - Split the text to highlight into words (separated by spaces) + // - Color each word + // - Join back the words (with spaces) + // - Join back the lines (with new lines) + const lines = text.split("\n").map((line) => { + let words = line.split(" "); + return words + .map((x) => { + if (x === "") { + return ""; + } + return colorFn(x); + }) + .join(" "); + }); + + text = lines.join("\n"); + } else { + // If the the to highline is in a single line we can just color the whole thing + text = colorFn(text); + } return [...head, text, ...compliment, ...tail]; } From dec408898f0b41cc98048c53c6ff891bc0424bcd Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 20 Mar 2024 17:58:15 -0300 Subject: [PATCH 33/79] Print additions and deletions with multiple segments --- src/v2/printer.ts | 83 +++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/src/v2/printer.ts b/src/v2/printer.ts index c04cd4a..572d597 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -21,47 +21,48 @@ export function applyChangesToSources( const { iterA, iterB } = _context; for (const change of changes) { + // TODO-2 refactor to reuse code switch (change.type) { case DiffType.addition: { - assert(change.segments.length === 1); - - const { b } = getIndexesFromSegment(change.segments[0]); + for (const segment of change.segments) { + const { b } = getIndexesFromSegment(segment); - const { start: startIndex, end: endIndex } = b; + const { start: startIndex, end: endIndex } = b; - const [start, end] = getStringPositionsFromRange( - iterB, - startIndex, - endIndex - 1, - ); + const [start, end] = getStringPositionsFromRange( + iterB, + startIndex, + endIndex - 1, + ); - charsB = getSourceWithChange( - charsB, - start, - end, - renderFn[DiffType.addition], - ); + charsB = getSourceWithChange( + charsB, + start, + end, + renderFn[DiffType.addition], + ); + } break; } case DiffType.deletion: { - assert(change.segments.length === 1); - - const { a } = getIndexesFromSegment(change.segments[0]); - - const { start: startIndex, end: endIndex } = a; - const [start, end] = getStringPositionsFromRange( - iterA, - startIndex, - endIndex - 1, - ); - - charsA = getSourceWithChange( - charsA, - start, - end, - renderFn[DiffType.deletion], - ); + for (const segment of change.segments) { + const { a } = getIndexesFromSegment(segment); + + const { start: startIndex, end: endIndex } = a; + const [start, end] = getStringPositionsFromRange( + iterA, + startIndex, + endIndex - 1, + ); + + charsA = getSourceWithChange( + charsA, + start, + end, + renderFn[DiffType.deletion], + ); + } break; } @@ -270,11 +271,23 @@ export function prettyPrintSources(a: string, b: string) { } export function prettyPrintChanges(a: string, b: string, changes: Change[]) { - const sourcesWithChanges = applyChangesToSources(a, b, changes, prettyRenderFn); - console.log(createTextTable(sourcesWithChanges.sourceA, sourcesWithChanges.sourceB)); + const sourcesWithChanges = applyChangesToSources( + a, + b, + changes, + prettyRenderFn, + ); + console.log( + createTextTable(sourcesWithChanges.sourceA, sourcesWithChanges.sourceB), + ); } -export function prettyPrintChangesInSequence(a: string, b: string, changes: Change[], options: { sortByLength: boolean } = { sortByLength: true }) { +export function prettyPrintChangesInSequence( + a: string, + b: string, + changes: Change[], + options: { sortByLength: boolean } = { sortByLength: true }, +) { const table = new Table({ head: [ colorFn.magenta("Type"), From dd17d7eaf0601bb92f5a6e5f49c5c732ef6e4faa Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 20 Mar 2024 19:33:45 -0300 Subject: [PATCH 34/79] Simplified printer code --- src/v2/printer.ts | 112 ++++++++++++++-------------------------------- 1 file changed, 34 insertions(+), 78 deletions(-) diff --git a/src/v2/printer.ts b/src/v2/printer.ts index 572d597..8f9ca4d 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -21,87 +21,43 @@ export function applyChangesToSources( const { iterA, iterB } = _context; for (const change of changes) { - // TODO-2 refactor to reuse code - switch (change.type) { - case DiffType.addition: { - for (const segment of change.segments) { - const { b } = getIndexesFromSegment(segment); - - const { start: startIndex, end: endIndex } = b; - - const [start, end] = getStringPositionsFromRange( - iterB, - startIndex, - endIndex - 1, - ); - - charsB = getSourceWithChange( - charsB, - start, - end, - renderFn[DiffType.addition], - ); - } - break; + for (const segment of change.segments) { + if (TypeMasks.AddOrMove & change.type) { + const { b } = getIndexesFromSegment(segment); + + const { start: startIndex, end: endIndex } = b; + + const [start, end] = getStringPositionsFromRange( + iterB, + startIndex, + endIndex - 1, + ); + + charsB = getSourceWithChange( + charsB, + start, + end, + renderFn[DiffType.addition], + ); } - case DiffType.deletion: { - for (const segment of change.segments) { - const { a } = getIndexesFromSegment(segment); - - const { start: startIndex, end: endIndex } = a; - const [start, end] = getStringPositionsFromRange( - iterA, - startIndex, - endIndex - 1, - ); - - charsA = getSourceWithChange( - charsA, - start, - end, - renderFn[DiffType.deletion], - ); - } - break; + if (TypeMasks.DelOrMove & change.type) { + const { a } = getIndexesFromSegment(segment); + + const { start: startIndex, end: endIndex } = a; + const [start, end] = getStringPositionsFromRange( + iterA, + startIndex, + endIndex - 1, + ); + + charsA = getSourceWithChange( + charsA, + start, + end, + renderFn[DiffType.deletion], + ); } - - case DiffType.move: { - for (const segment of change.segments) { - const { a, b } = getIndexesFromSegment(segment); - - const { start: startIndexA, end: endIndexA } = a; - const { start: startIndexB, end: endIndexB } = b; - - const [startA, endA] = getStringPositionsFromRange( - iterA, - startIndexA, - endIndexA - 1, - ); - const [startB, endB] = getStringPositionsFromRange( - iterB, - startIndexB, - endIndexB - 1, - ); - - charsA = getSourceWithChange( - charsA, - startA, - endA, - renderFn[DiffType.move], - ); - charsB = getSourceWithChange( - charsB, - startB, - endB, - renderFn[DiffType.move], - ); - } - break; - } - - default: - fail(`Unhandled type "${change.type}"`); } } From 498e566f6f334e73339bf7c308d89f5590e99a66 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Thu, 21 Mar 2024 18:01:41 -0300 Subject: [PATCH 35/79] Changed segment length for text length in finding a better match --- src/v2/change.ts | 4 ++-- src/v2/diff.ts | 6 +++--- src/v2/semanticAligment.ts | 4 ++-- src/v2/types.ts | 2 +- src/v2/utils.ts | 25 +++++++++++++++++++------ 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/v2/change.ts b/src/v2/change.ts index bb1dd12..8de8ea8 100644 --- a/src/v2/change.ts +++ b/src/v2/change.ts @@ -2,7 +2,7 @@ import { Node } from "./node"; import { Side } from "../shared/language"; import { DiffType, TypeMasks } from "../types"; import { CandidateMatch, Segment } from "./types"; -import { calculateCandidateMatchLength, getIndexesFromSegment } from "./utils"; +import { getTextLengthFromSegments, getIndexesFromSegment } from "./utils"; import { assert } from "../debug"; import { _context } from "."; @@ -15,7 +15,7 @@ export class Change { public skips = 0, ) { this.startNode = getStarterNode(type, segments); - this.length = calculateCandidateMatchLength(segments); + this.length = getTextLengthFromSegments(segments); } static createAddition(segments: Segment[]) { diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 43991a9..b40a7ab 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -3,7 +3,7 @@ import { assert, fail } from "../debug"; import { Iterator } from "./iterator"; import { range, rangeEq } from "../utils"; import { Node } from "./node"; -import { calculateCandidateMatchLength, equals, getAllNodesFromMatch, getEmptyCandidate, getIndexesFromSegment, isLatterCandidateBetter } from "./utils"; +import { getTextLengthFromSegments, equals, getAllNodesFromMatch, getEmptyCandidate, getIndexesFromSegment, isLatterCandidateBetter } from "./utils"; import { CandidateMatch, Segment } from "./types"; /** @@ -18,7 +18,7 @@ export function getBestMatch(nodeB: Node): CandidateMatch | undefined { } let bestMatch: CandidateMatch = { - length: 0, + textLength: 0, skips: 0, segments: [], }; @@ -225,7 +225,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { } return { - length: calculateCandidateMatchLength(segments), + textLength: getTextLengthFromSegments(segments), skips, segments, }; diff --git a/src/v2/semanticAligment.ts b/src/v2/semanticAligment.ts index a00c408..e1f9195 100644 --- a/src/v2/semanticAligment.ts +++ b/src/v2/semanticAligment.ts @@ -4,7 +4,7 @@ import { DiffType } from "../types"; import { range, rangeEq } from "../utils"; import { Change } from "./change"; import { Segment } from "./types"; -import { calculateCandidateMatchLength, getIndexesFromSegment } from "./utils"; +import { getTextLengthFromSegments, getIndexesFromSegment } from "./utils"; export interface Offset { index: number; @@ -45,7 +45,7 @@ export function computeMoveAlignment(changes: Change[]): Change[] { } else { if (unalignedSegments.length !== change.length) { change.segments = unalignedSegments; - change.length = calculateCandidateMatchLength(unalignedSegments); + change.length = getTextLengthFromSegments(unalignedSegments); } unalignedChanges.push(change); diff --git a/src/v2/types.ts b/src/v2/types.ts index 9d0aeea..9c39a67 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -32,7 +32,7 @@ export interface ResultTypeMapper { export type Segment = [indexA: number, indexB: number, length: number]; export interface CandidateMatch { - length: number; + textLength: number; skips: number; segments: Segment[]; } diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 7719d05..bc7b498 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -1,7 +1,7 @@ import { _context } from "."; import { assert } from "../debug"; import { Side } from "../shared/language"; -import { range } from "../utils"; +import { range, rangeEq } from "../utils"; import { Change } from "./change"; import { Iterator } from "./iterator"; import { Node } from "./node"; @@ -29,9 +29,9 @@ export function equals(nodeOne: Node, nodeTwo: Node): boolean { * TODO: MAybe compute a score fn */ export function isLatterCandidateBetter(currentCandidate: CandidateMatch, newCandidate: CandidateMatch): boolean { - if (newCandidate.length > currentCandidate.length) { + if (newCandidate.textLength > currentCandidate.textLength) { return true; - } else if (newCandidate.length < currentCandidate.length) { + } else if (newCandidate.textLength < currentCandidate.textLength) { return false; } @@ -44,11 +44,24 @@ export function isLatterCandidateBetter(currentCandidate: CandidateMatch, newCan return newCandidate.skips < currentCandidate.skips; } -export function calculateCandidateMatchLength(segments: Segment[]) { +// This function iterate for all the nodes (on both side if needed) of the match adding up the text length +export function getTextLengthFromSegments(segments: Segment[]) { let sum = 0; for (const segment of segments) { - sum += segment[2]; + const { a, b } = getIndexesFromSegment(segment) + + if (a.start !== -1) { + for (const i of range(a.start, a.end)) { + sum += _context.iterA.nodes[i].text.length + } + } + + if (b.start !== -1) { + for (const i of range(b.start, b.end)) { + sum += _context.iterB.nodes[i].text.length + } + } } assert(sum > 0, () => "Segment length is 0"); @@ -58,7 +71,7 @@ export function calculateCandidateMatchLength(segments: Segment[]) { export function getEmptyCandidate(): CandidateMatch { return { - length: 0, + textLength: 0, segments: [], skips: 0, }; From a25bbd04b6e711ea5bcfe1286bdbc637506b95a7 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 22 Mar 2024 12:34:04 -0300 Subject: [PATCH 36/79] Move match to a class, separate moves from changes --- src/v2/change.ts | 16 +++++---- src/v2/core.ts | 10 +++--- src/v2/diff.ts | 25 +++++-------- src/v2/index.ts | 11 +++--- src/v2/match.ts | 28 +++++++++++++++ src/v2/printer.ts | 4 +-- src/v2/semanticAligment.ts | 22 ++++-------- src/v2/types.ts | 6 ---- src/v2/utils.ts | 73 ++++++++++++++++++++------------------ 9 files changed, 104 insertions(+), 91 deletions(-) create mode 100644 src/v2/match.ts diff --git a/src/v2/change.ts b/src/v2/change.ts index 8de8ea8..c498ffe 100644 --- a/src/v2/change.ts +++ b/src/v2/change.ts @@ -1,21 +1,21 @@ import { Node } from "./node"; -import { Side } from "../shared/language"; import { DiffType, TypeMasks } from "../types"; -import { CandidateMatch, Segment } from "./types"; -import { getTextLengthFromSegments, getIndexesFromSegment } from "./utils"; +import { Segment } from "./types"; +import { CandidateMatch } from "./match"; +import { getIndexesFromSegment, getTextLengthFromSegments } from "./utils"; import { assert } from "../debug"; import { _context } from "."; -export class Change { +export class Change { startNode: Node; - length: number; + textLength: number; constructor( - public type: DiffType, + public type: Type, public segments: Segment[], public skips = 0, ) { this.startNode = getStarterNode(type, segments); - this.length = getTextLengthFromSegments(segments); + this.textLength = getTextLengthFromSegments(segments); } static createAddition(segments: Segment[]) { @@ -33,6 +33,8 @@ export class Change { } } +export type Move = Change; + function getStarterNode(type: DiffType, segments: Segment[]) { const { a, b } = getIndexesFromSegment(segments[0]); diff --git a/src/v2/core.ts b/src/v2/core.ts index a09f0d0..002861f 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -1,13 +1,13 @@ import { _context } from "."; import { getBestMatch, getSubSequenceNodes } from "./diff"; -import { getIndexesFromSegment, isLatterCandidateBetter } from "./utils"; +import { getIndexesFromSegment, sort } from "./utils"; import { Iterator } from "./iterator"; import { DiffType } from "../types"; import { Change } from "./change"; import { range } from "../utils"; export function computeDiff() { - const { iterA, iterB, changes, additions } = _context; + const { iterA, iterB, moves, additions } = _context; while (true) { const a = iterA.next(); @@ -53,18 +53,18 @@ export function computeDiff() { continue; } - if (isLatterCandidateBetter(bestCandidate, newCandidate)) { + if (newCandidate.isBetterThan(bestCandidate)) { bestCandidate = newCandidate; } } const move = Change.createMove(bestCandidate); - changes.push(move); + moves.push(move); markMatched(move); continue; } - return changes; + return moves.sort((a, b) => sort.desc(a.textLength, b.textLength)); } function markMatched(change: Change) { diff --git a/src/v2/diff.ts b/src/v2/diff.ts index b40a7ab..034a8b8 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -3,8 +3,9 @@ import { assert, fail } from "../debug"; import { Iterator } from "./iterator"; import { range, rangeEq } from "../utils"; import { Node } from "./node"; -import { getTextLengthFromSegments, equals, getAllNodesFromMatch, getEmptyCandidate, getIndexesFromSegment, isLatterCandidateBetter } from "./utils"; -import { CandidateMatch, Segment } from "./types"; +import { equals, getAllNodesFromMatch, getIndexesFromSegment, getTextLengthFromSegments } from "./utils"; +import { Segment } from "./types"; +import { CandidateMatch } from "./match"; /** * Returns the longest possible match for the given node, this is including possible skips to improve the match length. @@ -17,16 +18,12 @@ export function getBestMatch(nodeB: Node): CandidateMatch | undefined { return; } - let bestMatch: CandidateMatch = { - textLength: 0, - skips: 0, - segments: [], - }; + let bestMatch = CandidateMatch.createEmpty(); for (const candidate of aSideCandidates) { const newCandidate = getCandidateMatch(candidate, nodeB); - if (isLatterCandidateBetter(bestMatch, newCandidate)) { + if (newCandidate.isBetterThan(bestMatch)) { bestMatch = newCandidate; } } @@ -164,7 +161,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { continue mainLoop; } - let bestCandidate = getEmptyCandidate(); + let bestCandidate = CandidateMatch.createEmpty(); // TODO instead of getBestMatch use getCandidateMatch so that it skips node if necessary // TODO also getBestMatch has hardcoded B side @@ -176,7 +173,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { fail(); } - if (isLatterCandidateBetter(bestCandidate, newCandidate)) { + if (newCandidate.isBetterThan(bestCandidate)) { // Give this input // A B C C D // 0 1 2 3 4 @@ -196,7 +193,7 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { fail(); } - if (isLatterCandidateBetter(bestCandidate, newCandidate)) { + if (newCandidate.isBetterThan(bestCandidate)) { skipsInLookaheadB += node.index - newB.index; bestCandidate = newCandidate; } @@ -224,9 +221,5 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { break mainLoop; } - return { - textLength: getTextLengthFromSegments(segments), - skips, - segments, - }; + return new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); } diff --git a/src/v2/index.ts b/src/v2/index.ts index f35a5ca..acd40d7 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -5,7 +5,7 @@ import { computeDiff } from "./core"; import { asciiRenderFn, fail, prettyRenderFn } from "../debug"; import { Options, OutputType, ResultTypeMapper, Segment } from "./types"; import { applyChangesToSources } from "./printer"; -import { Change } from "./change"; +import { Change, Move } from "./change"; import { computeMoveAlignment } from "./semanticAligment"; import { compactAndCreateDiff } from "./compact"; import { DiffType } from "../types"; @@ -21,21 +21,22 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so const iterA = new Iterator(sourceA, Side.a); const iterB = new Iterator(sourceB, Side.b); - let changes: Change[] = []; + let moves: Move[] = []; const deletions: Segment[] = []; const additions: Segment[] = []; - _context = new Context(sourceA, sourceB, iterA, iterB, changes, deletions, additions); + _context = new Context(sourceA, sourceB, iterA, iterB, moves, deletions, additions); - changes = computeDiff(); + moves = computeDiff(); if (_options.tryAlignMoves) { - changes = computeMoveAlignment(changes); + moves = computeMoveAlignment(moves); } // We compact all tracked additions and deletions into a single change with multiple segments, we also compact them if possible + const changes: Change[] = moves; if (_context.additions.length) { const additionsChange = compactAndCreateDiff(DiffType.addition, additions); changes.push(additionsChange); diff --git a/src/v2/match.ts b/src/v2/match.ts new file mode 100644 index 0000000..abf232f --- /dev/null +++ b/src/v2/match.ts @@ -0,0 +1,28 @@ +import { Segment } from "./types"; + +export class CandidateMatch { + constructor( + public segments: Segment[], + public textLength: number, + public skips = 0, + ) {} + + static createEmpty() { + return new CandidateMatch([], 0, 0); + } + + isBetterThan(otherCandidate: CandidateMatch) { + if (otherCandidate.textLength !== this.textLength) { + return otherCandidate.textLength < this.textLength; + } + + if (otherCandidate.segments.length !== this.segments.length) { + return otherCandidate.segments.length < this.segments.length; + } + + return otherCandidate.skips < this.skips; + } + + draw() { + } +} diff --git a/src/v2/printer.ts b/src/v2/printer.ts index 8f9ca4d..8fe7717 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -257,7 +257,7 @@ export function prettyPrintChangesInSequence( colAligns: ["center", "center", "center", "center"], }); - const sortedByLength = options.sortByLength ? changes.sort((a, b) => (a.length < b.length ? 1 : -1)) : changes; + const sortedByLength = options.sortByLength ? changes.sort((a, b) => (a.textLength < b.textLength ? 1 : -1)) : changes; const lineNumberString = getLinesOfCodeString(a, b); @@ -269,7 +269,7 @@ export function prettyPrintChangesInSequence( table.push([ changeColorFn(changeName), - colorFn.blue(change.length), + colorFn.blue(change.textLength), colorFn.grey(change.skips || "-"), colorFn.cyan(`"${change.startNode.text}"`), colorFn.yellow(lineNumberString), diff --git a/src/v2/semanticAligment.ts b/src/v2/semanticAligment.ts index e1f9195..20fb10c 100644 --- a/src/v2/semanticAligment.ts +++ b/src/v2/semanticAligment.ts @@ -1,10 +1,9 @@ -import { applyAlignments } from "../backend/printer"; import { Side } from "../shared/language"; import { DiffType } from "../types"; import { range, rangeEq } from "../utils"; -import { Change } from "./change"; +import { Change, Move } from "./change"; import { Segment } from "./types"; -import { getTextLengthFromSegments, getIndexesFromSegment } from "./utils"; +import { getIndexesFromSegment, getTextLengthFromSegments } from "./utils"; export interface Offset { index: number; @@ -20,15 +19,10 @@ const alignmentTable = { b: new Map() as OffsetsMap, }; -export function computeMoveAlignment(changes: Change[]): Change[] { - const unalignedChanges: Change[] = []; +export function computeMoveAlignment(changes: Move[]): Move[] { + const unalignedChanges: Move[] = []; for (const change of changes) { - if (change.type !== DiffType.move) { - unalignedChanges.push(change); - continue; - } - const unalignedSegments: Segment[] = []; for (const segment of change.segments) { @@ -43,9 +37,9 @@ export function computeMoveAlignment(changes: Change[]): Change[] { // Fully aligned continue; } else { - if (unalignedSegments.length !== change.length) { + if (unalignedSegments.length !== change.textLength) { change.segments = unalignedSegments; - change.length = getTextLengthFromSegments(unalignedSegments); + change.textLength = getTextLengthFromSegments(unalignedSegments); } unalignedChanges.push(change); @@ -80,10 +74,6 @@ function addAlignment(segment: Segment) { } } -type SegmentAlignmentCheckResult = - | { canBeAligned: false } - | { canBeAligned: true; offsets: Offset[]; sideToAlign: Side }; - function canSegmentBeAligned(segment: Segment): boolean { const { a, b } = getIndexesFromSegment(segment); diff --git a/src/v2/types.ts b/src/v2/types.ts index 9c39a67..1c8fd1f 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -30,9 +30,3 @@ export interface ResultTypeMapper { // Start is inclusive, end is not inclusive export type Segment = [indexA: number, indexB: number, length: number]; - -export interface CandidateMatch { - textLength: number; - skips: number; - segments: Segment[]; -} diff --git a/src/v2/utils.ts b/src/v2/utils.ts index bc7b498..439baba 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -1,11 +1,12 @@ import { _context } from "."; import { assert } from "../debug"; import { Side } from "../shared/language"; -import { range, rangeEq } from "../utils"; -import { Change } from "./change"; +import { range } from "../utils"; +import { Move } from "./change"; import { Iterator } from "./iterator"; import { Node } from "./node"; -import { CandidateMatch, Segment } from "./types"; +import { Segment } from "./types"; +import { CandidateMatch } from "./match"; export class Context { // Iterators will get stored once they are initialize, which happens later on the execution @@ -15,7 +16,7 @@ export class Context { public sourceB: string, public iterA: Iterator, public iterB: Iterator, - public changes: Change[], + public moves: Move[], public deletions: Segment[], public additions: Segment[], ) {} @@ -25,41 +26,22 @@ export function equals(nodeOne: Node, nodeTwo: Node): boolean { return nodeOne.kind === nodeTwo.kind && nodeOne.text === nodeTwo.text; } -/** - * TODO: MAybe compute a score fn - */ -export function isLatterCandidateBetter(currentCandidate: CandidateMatch, newCandidate: CandidateMatch): boolean { - if (newCandidate.textLength > currentCandidate.textLength) { - return true; - } else if (newCandidate.textLength < currentCandidate.textLength) { - return false; - } - - if (newCandidate.segments.length < currentCandidate.segments.length) { - return true; - } else if (newCandidate.segments.length < currentCandidate.segments.length) { - return false; - } - - return newCandidate.skips < currentCandidate.skips; -} - // This function iterate for all the nodes (on both side if needed) of the match adding up the text length export function getTextLengthFromSegments(segments: Segment[]) { let sum = 0; for (const segment of segments) { - const { a, b } = getIndexesFromSegment(segment) + const { a, b } = getIndexesFromSegment(segment); if (a.start !== -1) { for (const i of range(a.start, a.end)) { - sum += _context.iterA.nodes[i].text.length + sum += _context.iterA.nodes[i].text.length; } } if (b.start !== -1) { for (const i of range(b.start, b.end)) { - sum += _context.iterB.nodes[i].text.length + sum += _context.iterB.nodes[i].text.length; } } } @@ -69,14 +51,6 @@ export function getTextLengthFromSegments(segments: Segment[]) { return sum; } -export function getEmptyCandidate(): CandidateMatch { - return { - textLength: 0, - segments: [], - skips: 0, - }; -} - export function getAllNodesFromMatch(match: CandidateMatch, side = Side.b) { const iter = getIterFromSide(side); const readFrom = side === Side.a ? 0 : 1; @@ -119,3 +93,34 @@ export function getIndexesFromSegment(segment: Segment) { export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); } + +function compare(a: number, b: number) { + if (a < b) { + return -1; + } else { + return 1; + } +} + +const reversed = (x: 1 | -1) => x === 1 ? -1 : 1; + +function asc(a: any, b: any, property?: string) { + if (property) { + return compare(a[property], b[property]); + } else { + return compare(a, b); + } +} + +function desc(a: any, b: any, property?: string) { + if (property) { + return reversed(compare(a[property], b[property])); + } else { + return reversed(compare(a, b)); + } +} + +export const sort = { + asc, + desc, +}; From 9474698bb5e8173889d54c6ee5a1e9c69c84f7bc Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 22 Mar 2024 13:17:53 -0300 Subject: [PATCH 37/79] Simplified code --- tests/utils2.ts | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/utils2.ts b/tests/utils2.ts index ea93994..8f163ac 100644 --- a/tests/utils2.ts +++ b/tests/utils2.ts @@ -27,7 +27,7 @@ export function getTestFn(testFn: typeof getDiff2) { const skipStandardTest = testInfo.only === "inversed"; if (!skipStandardTest) { - vTest(`Test ${name}`, () => { + vTest(`${name}`, () => { const { sourceA: resultA, sourceB: resultB } = testFn(a, b, { outputType: OutputType.text }); validateDiff(expA || a, expB || b, resultA, resultB); @@ -37,7 +37,7 @@ export function getTestFn(testFn: typeof getDiff2) { const skipInversedTest = testInfo.only === "standard"; if (!skipInversedTest) { - vTest(`Test ${name} inverse`, () => { + vTest(`[Inverse] ${name}`, () => { const { sourceA: resultA, sourceB: resultB } = testFn(b, a, { outputType: OutputType.text }); const inversedExpectedA = getInversedExpectedResult(expB || b); @@ -49,21 +49,11 @@ export function getTestFn(testFn: typeof getDiff2) { }; } -export function getInversedExpectedResult(expected: string) { - return expected.split("").map((char) => { - if (char === "➖") { - return "➕"; - } else if (char === "➕") { - return "➖"; - } else { - return char; - } - }).join(""); +function getInversedExpectedResult(expected: string) { + return expected.replaceAll("➕", "➖").replaceAll("➖", "➕"); } -export const test = getTestFn(getDiff2); - -export function validateDiff( +function validateDiff( expectedA: string, expectedB: string, resultA: string, @@ -76,3 +66,5 @@ export function validateDiff( expect(trimLines(resultA)).toEqual(trimLines(expectedA)); expect(trimLines(resultB)).toEqual(trimLines(expectedB)); } + +export const test = getTestFn(getDiff2); \ No newline at end of file From 116be9d2215bb383eb2e705fc5fdc029b5247d09 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 22 Mar 2024 14:02:47 -0300 Subject: [PATCH 38/79] Wroking --- src/v2/match.ts | 14 +++++++++----- src/v2/printer.ts | 5 +++-- src/v2/semanticAligment.ts | 31 ++++++++++++++++++------------- tests/utils2.ts | 33 ++++++++++++++++++++++----------- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/v2/match.ts b/src/v2/match.ts index abf232f..669c94f 100644 --- a/src/v2/match.ts +++ b/src/v2/match.ts @@ -12,15 +12,19 @@ export class CandidateMatch { } isBetterThan(otherCandidate: CandidateMatch) { - if (otherCandidate.textLength !== this.textLength) { - return otherCandidate.textLength < this.textLength; + if (this.textLength > otherCandidate.textLength) { + return true; + } else if (this.textLength < otherCandidate.textLength) { + return false; } - if (otherCandidate.segments.length !== this.segments.length) { - return otherCandidate.segments.length < this.segments.length; + if (this.segments.length < otherCandidate.segments.length) { + return true; + } else if (this.segments.length > otherCandidate.segments.length) { + return false; } - return otherCandidate.skips < this.skips; + return this.skips < otherCandidate.skips; } draw() { diff --git a/src/v2/printer.ts b/src/v2/printer.ts index 8fe7717..f1784ff 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -21,6 +21,7 @@ export function applyChangesToSources( const { iterA, iterB } = _context; for (const change of changes) { + const renderWith = renderFn[change.type]; for (const segment of change.segments) { if (TypeMasks.AddOrMove & change.type) { const { b } = getIndexesFromSegment(segment); @@ -37,7 +38,7 @@ export function applyChangesToSources( charsB, start, end, - renderFn[DiffType.addition], + renderWith, ); } @@ -55,7 +56,7 @@ export function applyChangesToSources( charsA, start, end, - renderFn[DiffType.deletion], + renderWith, ); } } diff --git a/src/v2/semanticAligment.ts b/src/v2/semanticAligment.ts index 20fb10c..748f4a2 100644 --- a/src/v2/semanticAligment.ts +++ b/src/v2/semanticAligment.ts @@ -14,20 +14,25 @@ export interface Offset { // number = index export type OffsetsMap = Map; -const alignmentTable = { - a: new Map() as OffsetsMap, - b: new Map() as OffsetsMap, -}; +interface AlignmentTable { + a: OffsetsMap; + b: OffsetsMap; +} export function computeMoveAlignment(changes: Move[]): Move[] { + const alignmentTable: AlignmentTable = { + a: new Map(), + b: new Map(), + }; + const unalignedChanges: Move[] = []; for (const change of changes) { const unalignedSegments: Segment[] = []; for (const segment of change.segments) { - if (canSegmentBeAligned(segment)) { - addAlignment(segment); + if (canSegmentBeAligned(alignmentTable, segment)) { + addAlignment(alignmentTable, segment); } else { unalignedSegments.push(segment); } @@ -49,11 +54,11 @@ export function computeMoveAlignment(changes: Move[]): Move[] { return unalignedChanges; } -function addAlignment(segment: Segment) { +function addAlignment(alignmentTable: AlignmentTable, segment: Segment) { const { a, b } = getIndexesFromSegment(segment); - const offsettedA = { start: getOffsettedIndex(Side.a, a.start), end: getOffsettedIndex(Side.a, a.end) }; - const offsettedB = { start: getOffsettedIndex(Side.b, b.start), end: getOffsettedIndex(Side.b, b.end) }; + const offsettedA = { start: getOffsettedIndex(alignmentTable, Side.a, a.start), end: getOffsettedIndex(alignmentTable, Side.a, a.end) }; + const offsettedB = { start: getOffsettedIndex(alignmentTable, Side.b, b.start), end: getOffsettedIndex(alignmentTable, Side.b, b.end) }; const indexDiff = Math.abs(offsettedA.start - offsettedB.start); @@ -74,11 +79,11 @@ function addAlignment(segment: Segment) { } } -function canSegmentBeAligned(segment: Segment): boolean { +function canSegmentBeAligned(alignmentTable: AlignmentTable, segment: Segment): boolean { const { a, b } = getIndexesFromSegment(segment); - const offsettedAIndex = getOffsettedIndex(Side.a, a.start); - const offsettedBIndex = getOffsettedIndex(Side.b, b.start); + const offsettedAIndex = getOffsettedIndex(alignmentTable, Side.a, a.start); + const offsettedBIndex = getOffsettedIndex(alignmentTable, Side.b, b.start); if (offsettedAIndex === offsettedBIndex) { return true; @@ -104,7 +109,7 @@ function canSegmentBeAligned(segment: Segment): boolean { return true; } -function getOffsettedIndex(side: Side, targetIndex: number) { +function getOffsettedIndex(alignmentTable: AlignmentTable, side: Side, targetIndex: number) { const ogIndex = targetIndex; const _side = alignmentTable[side]; diff --git a/tests/utils2.ts b/tests/utils2.ts index 8f163ac..42752c2 100644 --- a/tests/utils2.ts +++ b/tests/utils2.ts @@ -27,7 +27,7 @@ export function getTestFn(testFn: typeof getDiff2) { const skipStandardTest = testInfo.only === "inversed"; if (!skipStandardTest) { - vTest(`${name}`, () => { + vTest(`[Standard] ${name}`, () => { const { sourceA: resultA, sourceB: resultB } = testFn(a, b, { outputType: OutputType.text }); validateDiff(expA || a, expB || b, resultA, resultB); @@ -37,20 +37,32 @@ export function getTestFn(testFn: typeof getDiff2) { const skipInversedTest = testInfo.only === "standard"; if (!skipInversedTest) { - vTest(`[Inverse] ${name}`, () => { - const { sourceA: resultA, sourceB: resultB } = testFn(b, a, { outputType: OutputType.text }); + vTest(`[Inversed] ${name}`, () => { + const { sourceA: resultAA, sourceB: resultBB } = testFn(b, a, { outputType: OutputType.text }); const inversedExpectedA = getInversedExpectedResult(expB || b); const inversedExpectedB = getInversedExpectedResult(expA || a); - validateDiff(inversedExpectedA, inversedExpectedB, resultA, resultB); + validateDiff(inversedExpectedA, inversedExpectedB, resultAA, resultBB); }); } }; } function getInversedExpectedResult(expected: string) { - return expected.replaceAll("➕", "➖").replaceAll("➖", "➕"); + return expected.split("").map((char) => { + if (char === "➖") { + return "➕"; + } else if (char === "➕") { + return "➖"; + } else { + return char; + } + }).join(""); +} + +function trimLines(text: string) { + return text.split("\n").map((s) => s.trim()).join(""); } function validateDiff( @@ -59,12 +71,11 @@ function validateDiff( resultA: string, resultB: string, ) { - function trimLines(text: string) { - return text.split("\n").map((s) => s.trim()).join(""); - } + // expect(trimLines(resultA)).toEqual(trimLines(expectedA)); + // expect(trimLines(resultB)).toEqual(trimLines(expectedB)); - expect(trimLines(resultA)).toEqual(trimLines(expectedA)); - expect(trimLines(resultB)).toEqual(trimLines(expectedB)); + expect(resultA).toEqual(expectedA); + expect(resultB).toEqual(expectedB); } -export const test = getTestFn(getDiff2); \ No newline at end of file +export const test = getTestFn(getDiff2); From e976ed0eb29a0f3c229dfcb115f58c8b2e536a7e Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 22 Mar 2024 14:29:10 -0300 Subject: [PATCH 39/79] More fixes --- src/v2/index.ts | 4 ++-- src/v2/printer.ts | 35 +++++++++++++---------------------- tests/utils2.ts | 13 ++++++------- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/src/v2/index.ts b/src/v2/index.ts index acd40d7..ed8f961 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -52,10 +52,10 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so return changes as ResultTypeMapper[_OutputType]; } case OutputType.text: { - return applyChangesToSources(sourceA, sourceB, changes, asciiRenderFn) as ResultTypeMapper[_OutputType]; + return applyChangesToSources(sourceA, sourceB, changes, false) as ResultTypeMapper[_OutputType]; } case OutputType.prettyText: { - return applyChangesToSources(sourceA, sourceB, changes, prettyRenderFn) as ResultTypeMapper[_OutputType]; + return applyChangesToSources(sourceA, sourceB, changes, true) as ResultTypeMapper[_OutputType]; } default: fail(); diff --git a/src/v2/printer.ts b/src/v2/printer.ts index f1784ff..4b00f58 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -13,8 +13,9 @@ export function applyChangesToSources( sourceA: string, sourceB: string, changes: Change[], - renderFn = asciiRenderFn, + forDebug: boolean, ) { + const renderFn = forDebug ? prettyRenderFn : asciiRenderFn; let charsA = sourceA.split(""); let charsB = sourceB.split(""); @@ -39,6 +40,7 @@ export function applyChangesToSources( start, end, renderWith, + forDebug, ); } @@ -57,6 +59,7 @@ export function applyChangesToSources( start, end, renderWith, + forDebug, ); } } @@ -70,6 +73,8 @@ export function getSourceWithChange( start: number, end: number, colorFn: RenderFn, + // This will be true for `prettyRenderFn`, it enables patching the string in a way that it looks nice on the table. This patching would break the tests + forDebug: boolean, ) { // This is to handle cases like EndOfFile // TODO: Think if this is the best way to handle this, maybe we can just ignore the EOF node altogether or modify it @@ -94,7 +99,9 @@ export function getSourceWithChange( const compliment = getComplimentArray(charsToAdd); - if (text.includes("\n")) { + if (!forDebug) { + text = colorFn(text); + } else if (text.includes("\n")) { // One edge case we need to be aware of is that the text highlighted may be across newlines so simply coloring the whole segment won't work, for example: // // (COLOR_START) a @@ -108,23 +115,7 @@ export function getSourceWithChange( // // So that when splitted you get ["(COLOR_START) a (COLOR END)", "(COLOR_START) b (COLOR END)"] which produces the desired output - // Here we will perform the above mentioned by: - // - Iterate for each line of code (separated by new lines) - // - Split the text to highlight into words (separated by spaces) - // - Color each word - // - Join back the words (with spaces) - // - Join back the lines (with new lines) - const lines = text.split("\n").map((line) => { - let words = line.split(" "); - return words - .map((x) => { - if (x === "") { - return ""; - } - return colorFn(x); - }) - .join(" "); - }); + const lines = text.split("\n").map(colorFn); text = lines.join("\n"); } else { @@ -204,7 +195,7 @@ export function getPrettyStringFromChange( a.start, a.end - 1, ); - charsA = getSourceWithChange(charsA, startA, endA, color); + charsA = getSourceWithChange(charsA, startA, endA, color, true); } if (change.type & TypeMasks.AddOrMove) { @@ -213,7 +204,7 @@ export function getPrettyStringFromChange( b.start, b.end - 1, ); - charsB = getSourceWithChange(charsB, startB, endB, color); + charsB = getSourceWithChange(charsB, startB, endB, color, true); } } @@ -232,7 +223,7 @@ export function prettyPrintChanges(a: string, b: string, changes: Change[]) { a, b, changes, - prettyRenderFn, + true, ); console.log( createTextTable(sourcesWithChanges.sourceA, sourcesWithChanges.sourceB), diff --git a/tests/utils2.ts b/tests/utils2.ts index 42752c2..93ecd85 100644 --- a/tests/utils2.ts +++ b/tests/utils2.ts @@ -30,7 +30,7 @@ export function getTestFn(testFn: typeof getDiff2) { vTest(`[Standard] ${name}`, () => { const { sourceA: resultA, sourceB: resultB } = testFn(a, b, { outputType: OutputType.text }); - validateDiff(expA || a, expB || b, resultA, resultB); + validateDiff(a, b, expA || a, expB || b, resultA, resultB); }); } @@ -43,7 +43,7 @@ export function getTestFn(testFn: typeof getDiff2) { const inversedExpectedA = getInversedExpectedResult(expB || b); const inversedExpectedB = getInversedExpectedResult(expA || a); - validateDiff(inversedExpectedA, inversedExpectedB, resultAA, resultBB); + validateDiff(b, a, inversedExpectedA, inversedExpectedB, resultAA, resultBB); }); } }; @@ -66,16 +66,15 @@ function trimLines(text: string) { } function validateDiff( + sourceA: string, + sourceB: string, expectedA: string, expectedB: string, resultA: string, resultB: string, ) { - // expect(trimLines(resultA)).toEqual(trimLines(expectedA)); - // expect(trimLines(resultB)).toEqual(trimLines(expectedB)); - - expect(resultA).toEqual(expectedA); - expect(resultB).toEqual(expectedB); + expect(trimLines(resultA), sourceA).toEqual(trimLines(expectedA)); + expect(trimLines(resultB), sourceB).toEqual(trimLines(expectedB)); } export const test = getTestFn(getDiff2); From 8b987df6e247ff28b28b74e0e1ecd583db58e76c Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 22 Mar 2024 16:48:24 -0300 Subject: [PATCH 40/79] Enabled first tests --- package.json | 1 + src/v2/diff.ts | 25 +++++++++++++------- tests/conformance/additions-removals.test.ts | 5 +--- tests/conformance/alignment.test.ts | 7 +++--- tests/conformance/move.test.ts | 2 +- tests/conformance/sequence-matching.test.ts | 2 +- tests/conformance/trivia-diff.test.ts | 8 +++---- tests/conformance/trivia-format.test.ts | 5 ++-- tests/utils2.ts | 2 +- 9 files changed, 31 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index e30e8ec..a47ffc4 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "scripts": { "test": "vitest -c ./vitest.config.js", + "testNew": "vitest -c ./vitest.config.js additions-removals changes", "test-v2": "vitest -c ./vitest.config.js ./tests/v2", "build": "tsc --watch", "check": "npm run format && npm run lint", diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 034a8b8..4d4f8b0 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -1,27 +1,32 @@ import { _context } from "."; import { assert, fail } from "../debug"; import { Iterator } from "./iterator"; -import { range, rangeEq } from "../utils"; +import { oppositeSide, range, rangeEq } from "../utils"; import { Node } from "./node"; -import { equals, getAllNodesFromMatch, getIndexesFromSegment, getTextLengthFromSegments } from "./utils"; +import { equals, getAllNodesFromMatch, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments } from "./utils"; import { Segment } from "./types"; import { CandidateMatch } from "./match"; +import { Side } from "../shared/language"; /** * Returns the longest possible match for the given node, this is including possible skips to improve the match length. */ -export function getBestMatch(nodeB: Node): CandidateMatch | undefined { - const aSideCandidates = _context.iterA.getMatchingNodes(nodeB); - - // The given B node wasn't found, it was added - if (aSideCandidates.length === 0) { +export function getBestMatch(node: Node): CandidateMatch | undefined { + // Since this function can be called with both A-sided and B-sided nodes, we need to get the iter dynamically + // The iterator we need is the opposite of the given node since there is where we need to get the match from + const sideToLook = oppositeSide(node.side); + const iter = getIterFromSide(sideToLook); + const otherSideCandidates = iter.getMatchingNodes(node); + + // The given node wasn't found, it was added + if (otherSideCandidates.length === 0) { return; } let bestMatch = CandidateMatch.createEmpty(); - for (const candidate of aSideCandidates) { - const newCandidate = getCandidateMatch(candidate, nodeB); + for (const candidate of otherSideCandidates) { + const newCandidate = node.side === Side.a ? getCandidateMatch(node, candidate) : getCandidateMatch(candidate, node); if (newCandidate.isBetterThan(bestMatch)) { bestMatch = newCandidate; @@ -53,6 +58,8 @@ export function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { const MAX_NODE_SKIPS = 5; export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { + assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); + const segments: Segment[] = []; const { iterA, iterB } = _context; diff --git a/tests/conformance/additions-removals.test.ts b/tests/conformance/additions-removals.test.ts index f78f713..f00c6f9 100644 --- a/tests/conformance/additions-removals.test.ts +++ b/tests/conformance/additions-removals.test.ts @@ -1,5 +1,5 @@ import { describe } from "vitest"; -import { test } from "../utils"; +import { test } from "../utils2"; describe("Properly report lines added and removed", () => { test({ @@ -26,9 +26,6 @@ describe("Properly report lines added and removed", () => { let age; let name; `, - expA: ` - let name; - `, expB: ` ➕let age;➕ let name; diff --git a/tests/conformance/alignment.test.ts b/tests/conformance/alignment.test.ts index ab01340..47158ed 100644 --- a/tests/conformance/alignment.test.ts +++ b/tests/conformance/alignment.test.ts @@ -1,9 +1,8 @@ import { describe } from "vitest"; -import { getTestFn } from "../utils"; -import { getDiff, OutputType } from "../../src"; -import colorFn from "kleur"; +import { getTestFn } from "../utils2"; +import { getDiff2 } from "../../src/v2"; -const test = getTestFn(getDiff, { outputType: OutputType.alignedText, alignmentText: colorFn.cyan("<>"), ignoreChangeMarkers: true }); +const test = getTestFn(getDiff2); //{ outputType: OutputType.alignedText, alignmentText: colorFn.cyan("<>"), ignoreChangeMarkers: true } describe.skip("Properly align code", () => { test({ diff --git a/tests/conformance/move.test.ts b/tests/conformance/move.test.ts index 1f66004..a4c4a6a 100644 --- a/tests/conformance/move.test.ts +++ b/tests/conformance/move.test.ts @@ -1,5 +1,5 @@ import { describe } from "vitest"; -import { test } from "../utils"; +import { test } from "../utils2"; describe("Properly report lines added", () => { test({ diff --git a/tests/conformance/sequence-matching.test.ts b/tests/conformance/sequence-matching.test.ts index 391235b..67270e7 100644 --- a/tests/conformance/sequence-matching.test.ts +++ b/tests/conformance/sequence-matching.test.ts @@ -1,5 +1,5 @@ import { describe, test as vTest } from "vitest"; -import { test } from "../utils"; +import { test } from "../utils2"; describe("Properly report moves in a same sequence", () => { test({ diff --git a/tests/conformance/trivia-diff.test.ts b/tests/conformance/trivia-diff.test.ts index f7eb192..13d375d 100644 --- a/tests/conformance/trivia-diff.test.ts +++ b/tests/conformance/trivia-diff.test.ts @@ -1,6 +1,6 @@ import { describe, test } from "vitest"; import { getDiff } from "../../src"; -import { validateDiff } from "../utils"; +import { validateDiff } from "../utils2"; describe("Ignore trivia", () => { test("Case 1", () => { @@ -15,7 +15,7 @@ describe("Ignore trivia", () => { const { sourceA, sourceB } = getDiff(a, b); - validateDiff(resultA, resultB, sourceA, sourceB); + validateDiff(a, b, resultA, resultB, sourceA, sourceB); }); test("Case 2", () => { @@ -30,7 +30,7 @@ describe("Ignore trivia", () => { const { sourceA, sourceB } = getDiff(a, b); - validateDiff(resultA, resultB, sourceA, sourceB); + validateDiff(a, b, resultA, resultB, sourceA, sourceB); }); test("Case 3", () => { @@ -47,6 +47,6 @@ describe("Ignore trivia", () => { const { sourceA, sourceB } = getDiff(a, b); - validateDiff(resultA, resultB, sourceA, sourceB); + validateDiff(a, b, resultA, resultB, sourceA, sourceB); }); }); diff --git a/tests/conformance/trivia-format.test.ts b/tests/conformance/trivia-format.test.ts index 7e4b353..25612fd 100644 --- a/tests/conformance/trivia-format.test.ts +++ b/tests/conformance/trivia-format.test.ts @@ -1,8 +1,9 @@ import { describe } from "vitest"; -import { getTestFn } from "../utils"; +import { getTestFn } from "../utils2"; import { getDiff, OutputType } from "../../src"; +import { getDiff2 } from "../../src/v2"; -const test = getTestFn(getDiff, { outputType: OutputType.alignedText, alignmentText: " <>", ignoreChangeMarkers: true }); +const test = getTestFn(getDiff2); // { outputType: OutputType.alignedText, alignmentText: " <>", ignoreChangeMarkers: true } describe.skip("Properly align formatted code", () => { test({ diff --git a/tests/utils2.ts b/tests/utils2.ts index 93ecd85..f7fee57 100644 --- a/tests/utils2.ts +++ b/tests/utils2.ts @@ -65,7 +65,7 @@ function trimLines(text: string) { return text.split("\n").map((s) => s.trim()).join(""); } -function validateDiff( +export function validateDiff( sourceA: string, sourceB: string, expectedA: string, From d026eb4db1ce02364c2821f706f7ab10ccaaed77 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 22 Mar 2024 16:52:14 -0300 Subject: [PATCH 41/79] More test enabled with improved output --- tests/conformance/changes.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/conformance/changes.test.ts b/tests/conformance/changes.test.ts index 4f408a2..fc8b46f 100644 --- a/tests/conformance/changes.test.ts +++ b/tests/conformance/changes.test.ts @@ -1,5 +1,5 @@ import { describe } from "vitest"; -import { test } from "../utils"; +import { test } from "../utils2"; describe("Properly report line changed", () => { test({ @@ -43,10 +43,10 @@ describe("Properly report line changed", () => { let firstName = "elian" `, expA: ` - ➖let name➖ = "elian" + let ➖name➖ = "elian" `, expB: ` - ➕let firstName➕ = "elian" + let ➕firstName➕ = "elian" `, }); @@ -56,13 +56,13 @@ describe("Properly report line changed", () => { let name = "elian" `, b: ` - let name = "eliam" + let name = "fernando" `, expA: ` let name = ➖"elian"➖ `, expB: ` - let name = ➕"eliam"➕ + let name = ➕"fernando"➕ `, }); @@ -82,20 +82,19 @@ describe("Properly report line changed", () => { `, }); - // TODO: This test got downgraded with the inclusion of the single node matching policy, if we introduce an LCS skip count nodes we may regain the old output test({ name: "Multi line change", a: ` let name = "elian" `, b: ` - let firstName = "eliam" + let firstName = "fernando" `, expA: ` - ➖let name = "elian"➖ + let ➖name➖ = ➖"elian"➖ `, expB: ` - ➕let firstName = "eliam"➕ + let ➕firstName➕ = ➕"fernando"➕ `, }); }); From 0b843ac2f85ca504d1324d4cafbccc87c9ef28fa Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 22 Mar 2024 17:15:54 -0300 Subject: [PATCH 42/79] more working --- package.json | 2 +- tests/conformance/move.test.ts | 693 ++++++++++++++++----------------- tests/realWorld/case3/notes.md | 2 +- tests/v2/tree-walker.test.ts | 159 -------- v2.md | 99 +++-- 5 files changed, 409 insertions(+), 546 deletions(-) delete mode 100644 tests/v2/tree-walker.test.ts diff --git a/package.json b/package.json index a47ffc4..349f8ba 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "test": "vitest -c ./vitest.config.js", - "testNew": "vitest -c ./vitest.config.js additions-removals changes", + "testNew": "vitest -c ./vitest.config.js additions-removals changes move", "test-v2": "vitest -c ./vitest.config.js ./tests/v2", "build": "tsc --watch", "check": "npm run format && npm run lint", diff --git a/tests/conformance/move.test.ts b/tests/conformance/move.test.ts index a4c4a6a..51bc28c 100644 --- a/tests/conformance/move.test.ts +++ b/tests/conformance/move.test.ts @@ -76,12 +76,7 @@ describe("Properly report lines added", () => { b: ` 1 2 - 'x' - 1 - 2 - 3 - `, - expA: ` + x 1 2 3 @@ -89,7 +84,7 @@ describe("Properly report lines added", () => { expB: ` ➕1 2 - 'x'➕ + x➕ 1 2 3 @@ -99,27 +94,21 @@ describe("Properly report lines added", () => { test({ name: "LCS case 2", a: ` - 'x' + x 1 2 3 `, b: ` - 'x' + x 1 2 1 2 3 `, - expA: ` - 'x' - 1 - 2 - 3 - `, expB: ` - 'x' + x 1 2 ➕1 @@ -144,355 +133,355 @@ describe("Properly report lines added", () => { `, }); - test({ - name: "Mix of move with deletions and additions 2", - a: ` - fn(x) - `, - b: ` - console.log(fn(1)) - `, - expA: ` - fn(➖x➖) - `, - expB: ` - ➕console.log(➕fn(➕1➕)➕)➕ - `, - }); + // test({ + // name: "Mix of move with deletions and additions 2", + // a: ` + // fn(x) + // `, + // b: ` + // console.log(fn(1)) + // `, + // expA: ` + // fn(➖x➖) + // `, + // expB: ` + // ➕console.log(➕fn(➕1➕)➕)➕ + // `, + // }); - test({ - name: "Mix of move with deletions and additions 3", - a: ` - console.log() && 3 - `, - b: ` - fn(console.log(2)) - `, - expA: ` - console.log() ➖&& 3➖ - `, - expB: ` - ➕fn(➕console.log(➕2➕)➕)➕ - `, - }); + // test({ + // name: "Mix of move with deletions and additions 3", + // a: ` + // console.log() && 3 + // `, + // b: ` + // fn(console.log(2)) + // `, + // expA: ` + // console.log() ➖&& 3➖ + // `, + // expB: ` + // ➕fn(➕console.log(➕2➕)➕)➕ + // `, + // }); - test({ - name: "Properly match closing paren", - a: ` - console.log() - `, - b: ` - console.log(fn()) - `, - expA: ` - console.log() - `, - expB: ` - console.log(➕fn()➕) - `, - }); + // test({ + // name: "Properly match closing paren", + // a: ` + // console.log() + // `, + // b: ` + // console.log(fn()) + // `, + // expA: ` + // console.log() + // `, + // expB: ` + // console.log(➕fn()➕) + // `, + // }); - test({ - name: "Properly match closing paren 2", - a: ` - if (true) { - print() - } - `, - b: ` - z - print(123) - x - `, - expA: ` - ➖if (true) {➖ - print() - ➖}➖ - `, - expB: ` - ➕z➕ - print(➕123➕) - ➕x➕ - `, - }); + // test({ + // name: "Properly match closing paren 2", + // a: ` + // if (true) { + // print() + // } + // `, + // b: ` + // z + // print(123) + // x + // `, + // expA: ` + // ➖if (true) {➖ + // print() + // ➖}➖ + // `, + // expB: ` + // ➕z➕ + // print(➕123➕) + // ➕x➕ + // `, + // }); - test({ - name: "Properly match closing paren 3", - a: ` - console.log() && 3 - `, - b: ` - function asd () { - console.log("hi") - } - `, - expA: ` - console.log() ➖&& 3➖ - `, - expB: ` - ➕function asd () {➕ - console.log(➕"hi"➕) - ➕}➕ - `, - }); + // test({ + // name: "Properly match closing paren 3", + // a: ` + // console.log() && 3 + // `, + // b: ` + // function asd () { + // console.log("hi") + // } + // `, + // expA: ` + // console.log() ➖&& 3➖ + // `, + // expB: ` + // ➕function asd () {➕ + // console.log(➕"hi"➕) + // ➕}➕ + // `, + // }); - test({ - name: "Properly match closing paren 4", - a: ` - 321 - if (true) { - print() - } - `, - b: ` - print(123) - `, - expA: ` - ➖321 - if (true) {➖ - print() - ➖}➖ - `, - expB: ` - print(➕123➕) - `, - }); + // test({ + // name: "Properly match closing paren 4", + // a: ` + // 321 + // if (true) { + // print() + // } + // `, + // b: ` + // print(123) + // `, + // expA: ` + // ➖321 + // if (true) {➖ + // print() + // ➖}➖ + // `, + // expB: ` + // print(➕123➕) + // `, + // }); - test({ - name: "Properly match closing paren 5", - a: ` - )fn(x) - `, - b: ` - console.log(fn(1)) - `, - expA: ` - ➖)➖fn(➖x➖) - `, - expB: ` - ➕console.log(➕fn(➕1➕)➕)➕ - `, - }); + // test({ + // name: "Properly match closing paren 5", + // a: ` + // )fn(x) + // `, + // b: ` + // console.log(fn(1)) + // `, + // expA: ` + // ➖)➖fn(➖x➖) + // `, + // expB: ` + // ➕console.log(➕fn(➕1➕)➕)➕ + // `, + // }); - test({ - name: "Properly match closing paren 6", - a: ` - x - const foo = { - a: 1 - } - `, - b: ` - function foo() { - return z - } - - function zor() { - return { - a: 1 - } - } - `, - expA: ` - ➖x - const➖ foo ➖=➖ { - a: 1 - } - `, - expB: ` - ➕function➕ foo➕() { - return z - } - - function zor() { - return➕ { - a: 1 - } - ➕}➕ - `, - }); + // test({ + // name: "Properly match closing paren 6", + // a: ` + // x + // const foo = { + // a: 1 + // } + // `, + // b: ` + // function foo() { + // return z + // } - // Test closing the paren on a deletion / addition on the "verifySingle" - test({ - name: "Properly match closing paren 7", - a: ` - function* range() { - while (i < end - 1) { - yield i; - } - } - `, - b: ` - console.log(1) - `, - expA: ` - ➖function* range() { - while (i < end -➖ 1➖) { - yield i; - } - }➖ - `, - expB: ` - ➕console.log(➕1➕)➕ - `, - }); + // function zor() { + // return { + // a: 1 + // } + // } + // `, + // expA: ` + // ➖x + // const➖ foo ➖=➖ { + // a: 1 + // } + // `, + // expB: ` + // ➕function➕ foo➕() { + // return z + // } - // Test closing the paren on a move with syntax error - test({ - name: "Properly match closing paren 8", - a: ` - function asd() { - 123 - 123 - x - } - `, - b: ` - function asd() { - 123 - 123 - Z - `, - expA: ` - function asd() { - 123 - 123 - ➖x - }➖ - `, - expB: ` - function asd() { - 123 - 123 - ➕Z➕ - `, - }); + // function zor() { + // return➕ { + // a: 1 + // } + // ➕}➕ + // `, + // }); - // Test for another syntax error, this hit the branch where we initialize a stack with a closing paren - test({ - name: "Properly match closing paren 9", - a: ` - { - `, - b: ` - }{ - `, - expA: ` - ➖{➖ - `, - expB: ` - ➕}{➕ - `, - }); + // // Test closing the paren on a deletion / addition on the "verifySingle" + // test({ + // name: "Properly match closing paren 7", + // a: ` + // function* range() { + // while (i < end - 1) { + // yield i; + // } + // } + // `, + // b: ` + // console.log(1) + // `, + // expA: ` + // ➖function* range() { + // while (i < end -➖ 1➖) { + // yield i; + // } + // }➖ + // `, + // expB: ` + // ➕console.log(➕1➕)➕ + // `, + // }); - // Test the ignore matches in the process moves - test({ - name: "Properly match closing paren 10", - a: ` - { - { a, b, x } = obj - } - `, - b: ` - { - { x } = obj - z - } - `, - expA: ` - { - { ➖a, b,➖ x } = obj - } - `, - expB: ` - { - { x } = obj - ➕z➕ - } - `, - }); + // // Test closing the paren on a move with syntax error + // test({ + // name: "Properly match closing paren 8", + // a: ` + // function asd() { + // 123 + // 123 + // x + // } + // `, + // b: ` + // function asd() { + // 123 + // 123 + // Z + // `, + // expA: ` + // function asd() { + // 123 + // 123 + // ➖x + // }➖ + // `, + // expB: ` + // function asd() { + // 123 + // 123 + // ➕Z➕ + // `, + // }); - // Also test the match ignoring logic, now inside the true branch on the alignment + // // Test for another syntax error, this hit the branch where we initialize a stack with a closing paren + // test({ + // name: "Properly match closing paren 9", + // a: ` + // { + // `, + // b: ` + // }{ + // `, + // expA: ` + // ➖{➖ + // `, + // expB: ` + // ➕}{➕ + // `, + // }); - test({ - name: "Properly match closing paren 11", - a: ` - { - () - 1 - } - `, - b: ` - x - { - (c) - } - `, - expA: ` - { - () - ➖1➖ - } - `, - expB: ` - ➕x➕ - { - (➕c➕) - } - `, - }); + // // Test the ignore matches in the process moves + // test({ + // name: "Properly match closing paren 10", + // a: ` + // { + // { a, b, x } = obj + // } + // `, + // b: ` + // { + // { x } = obj + // z + // } + // `, + // expA: ` + // { + // { ➖a, b,➖ x } = obj + // } + // `, + // expB: ` + // { + // { x } = obj + // ➕z➕ + // } + // `, + // }); - // Testing single node matching - test({ - name: "Noise reduction", - a: ` - function foo() { - const name = 123; - } - - function bar() { - return 123 - } - - let var1 = foo() - let var2 = bar() - `, - b: ` - const var1 = foo() - const var2 = bar() - `, - expA: ` - ➖function foo() { - const name = 123; - } - - function bar() { - return 123 - } - - let➖ var1 = foo() - ➖let➖ var2 = bar() - `, - expB: ` - ➕const➕ var1 = foo() - ➕const➕ var2 = bar() - `, - }); + // // Also test the match ignoring logic, now inside the true branch on the alignment - // This used to break the inverse - test({ - name: "Simple alignment", - a: ` - fn(-1) - `, - b: ` - (1) - `, - expA: ` - ➖fn(-➖1➖)➖ - `, - expB: ` - ➕(➕1➕)➕ - `, - }); + // test({ + // name: "Properly match closing paren 11", + // a: ` + // { + // () + // 1 + // } + // `, + // b: ` + // x + // { + // (c) + // } + // `, + // expA: ` + // { + // () + // ➖1➖ + // } + // `, + // expB: ` + // ➕x➕ + // { + // (➕c➕) + // } + // `, + // }); + + // // Testing single node matching + // test({ + // name: "Noise reduction", + // a: ` + // function foo() { + // const name = 123; + // } + + // function bar() { + // return 123 + // } + + // let var1 = foo() + // let var2 = bar() + // `, + // b: ` + // const var1 = foo() + // const var2 = bar() + // `, + // expA: ` + // ➖function foo() { + // const name = 123; + // } + + // function bar() { + // return 123 + // } + + // let➖ var1 = foo() + // ➖let➖ var2 = bar() + // `, + // expB: ` + // ➕const➕ var1 = foo() + // ➕const➕ var2 = bar() + // `, + // }); + + // // This used to break the inverse + // test({ + // name: "Simple alignment", + // a: ` + // fn(-1) + // `, + // b: ` + // (1) + // `, + // expA: ` + // ➖fn(-➖1➖)➖ + // `, + // expB: ` + // ➕(➕1➕)➕ + // `, + // }); }); diff --git a/tests/realWorld/case3/notes.md b/tests/realWorld/case3/notes.md index 317586e..aab02af 100644 --- a/tests/realWorld/case3/notes.md +++ b/tests/realWorld/case3/notes.md @@ -1 +1 @@ -This is an extract of the TS checker file, cutted into a broken small repro that used to crash \ No newline at end of file +This is an extract of the TS checker file, cutted into a broken small repro that used to crash diff --git a/tests/v2/tree-walker.test.ts b/tests/v2/tree-walker.test.ts deleted file mode 100644 index a0524ee..0000000 --- a/tests/v2/tree-walker.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { expect, test } from "vitest"; -import { Iterator } from "../../src/v2/iterator"; -import { Side } from "../../src/shared/language"; - -const source = "1 + (2 + 3)"; -const expectedNodes = [ - "SourceFile", - "SyntaxList", - "ExpressionStatement", - "BinaryExpression", - "NumericLiteral", - "PlusToken", - "ParenthesizedExpression", - "OpenParenToken", - "BinaryExpression", - "NumericLiteral", - "PlusToken", - "NumericLiteral", - "CloseParenToken", - "EndOfFileToken", -]; - -test("Ensure the tree walker visits all nodes in order", () => { - const iter = new Iterator(source, Side.a); - - // Prime the walker so that we skip the "SourceFile", we will use that to detect when the iteration start over - const nodes: string[] = [iter.next()!.prettyKind]; - - while (true) { - const next = iter.next()!.prettyKind; - - if (next === "SourceFile") { - break; - } - - nodes.push(next); - } - - expect(nodes).toMatchObject(expectedNodes); -}); - -test("Ensure the tree walker visits all nodes by passing one node that the time", () => { - const iter = new Iterator(source, Side.a); - - const nodes: string[] = []; - - const one = iter.next(); - nodes.push(one!.prettyKind); - const two = iter.next(one); - nodes.push(two!.prettyKind); - const three = iter.next(two); - nodes.push(three!.prettyKind); - const four = iter.next(three); - nodes.push(four!.prettyKind); - const five = iter.next(four); - nodes.push(five!.prettyKind); - const six = iter.next(five); - nodes.push(six!.prettyKind); - const seven = iter.next(six); - nodes.push(seven!.prettyKind); - const eight = iter.next(seven); - nodes.push(eight!.prettyKind); - const nine = iter.next(eight); - nodes.push(nine!.prettyKind); - const ten = iter.next(nine); - nodes.push(ten!.prettyKind); - const eleven = iter.next(ten); - nodes.push(eleven!.prettyKind); - const twelve = iter.next(eleven); - nodes.push(twelve!.prettyKind); - const thirteen = iter.next(twelve); - nodes.push(thirteen!.prettyKind); - const fourteen = iter.next(thirteen); - nodes.push(fourteen!.prettyKind); - - expect(nodes).toMatchObject(expectedNodes); -}); - -test("Ensure the tree walker loops back after visiting all the nodes", () => { - const iter = new Iterator(source, Side.a); - - let lastNode; - - let i = 0; - while (i < expectedNodes.length) { - lastNode = iter.next(); - i++; - } - - const shouldBeFirstNode = iter.next()!.prettyKind; - - expect(shouldBeFirstNode).toBe(expectedNodes[0]); -}); - -test("Ensure the tree walker ignores matched nodes", () => { - const iter = new Iterator(source, Side.a); - - // Given the full node list, lets ignore the following - const expectedNodes2 = [ - //"SourceFile", // index 0, root node - "SyntaxList", - "ExpressionStatement", - "BinaryExpression", - "NumericLiteral", - "PlusToken", - "ParenthesizedExpression", - //"OpenParenToken", // index 7, has siblings - "BinaryExpression", - "NumericLiteral", - "PlusToken", - //"NumericLiteral", // index 11, last sibling - "CloseParenToken", - "EndOfFileToken", - ]; - - const nodesToMatch = [0, 7, 11]; - - // First pass over the node to mark a few as matched - while (true) { - const node = iter.next()!; - - if (nodesToMatch.includes(node.id)) { - node.mark(); - } - - if (node.prettyKind === "EndOfFileToken") break; - } - - iter.resetWalkingOrder(); - - const nodes = []; - // Second pass to gather the non-matched nodes - while (true) { - const node = iter.next()!; - - nodes.push(node.prettyKind); - - if (node.prettyKind === "EndOfFileToken") break; - } - - expect(nodes).toMatchObject(expectedNodes2); -}); - -test("Ensure the tree walker returns undefined after all nodes are matched", () => { - const iter = new Iterator(source, Side.a); - - while (true) { - const node = iter.next()!; - node.mark(); - - if (node.prettyKind === "EndOfFileToken") break; - } - - iter.resetWalkingOrder(); - - expect(iter.next()).toBe(undefined); - expect(iter.next()).toBe(undefined); - expect(iter.next()).toBe(undefined); -}); diff --git a/v2.md b/v2.md index a40dc0b..e1c33e6 100644 --- a/v2.md +++ b/v2.md @@ -1,44 +1,55 @@ +# Tasks: +- Open close verifier +- Can node be matched alone +- Pass configs (compiler, lang name, openClose, singleMatchingNode) as language options + This is a high-level overview of the second version of the algorithm. I'm reworking the whole system to solve the following issues that arise during the development of the previous version and, despite the efforts, I couldn't build the existing architecture to solve them. So I'm starting from scratch with those limitations in mind, these are: # High prio -## Score based matching -Before we would only match perfect sequences, this means that the slightest of changes would disrupt the matching. -In the new version, we are going to have a score function that would assign a score from (tentatively) 0 to 100 and the highest scoring subsequence will be picked. +## Score based ranking of sequence s +The current heuristics are, in order: +- Sequence length (more is better) +- Number of segments (less is better) +- Number of skios (less is better) + +We should also consider +- Text distance: for example + 1 Z 2 + 1 ZZZ 2 + Both have 1 skips but the number of characters in between is different +- Position in text distnace: We could have multiple matches for a given sequece, some being closer by others being many lines of codes down bellow. We could prefer closer by matches +- Levenshtein distance -Some notes: -- Still need to define the score function, it could include the following data - - Text content - - Kind - - Line and position (close changes are preferred) - - Number of nodes in between (fewer nodes in between preferred) -- Score should be above a given threshold to match +This score function could give a number from 1 - 100, higher number meaning more similarities, then we take this number to compare matches + +Another application for a score function, perhaps not the same one, would be to try match as "Change", nodes that changed in the source code -Example ```ts -function asd() { - return 123 -} + function asd() { + return 123 + } -// vs + // vs -function ddd() { - return 333 -} -``` + function ddd() { + return 333 + } -This should match a single move + // This could be matched as a single move with the number literal as a change +``` -## Moves now contain sections -Before we would create a move saying, "From X to Y it moved to A and B, limiting us to a single section, this is why we had to include the closing node as a separate field. Also, this single-section approach will limit us considering the above point where now a move could be, for example: -"perfect match", "addition", "perfect match" +For this usecase the score function could consider things like +- Text context (myers or Levenshtein) +- Kind -So moves should have an array of sections +## Open-close verifier +TODO -TODO: Check if we can remove the open/close verifier with this +## canNodeBeMatchedAlone config -## Use all the AST nodesData changes -Right now we are using the "textual" nodes, but we should use the full AST. For example, to solve the semantic diff case that arises with JS ASI +## Check parent nodes to prevent semantic mistakes +Given: ```js function asd() { return 1 @@ -52,7 +63,19 @@ function asd() { } ``` -This will also simplify the language setup as you won't need to include which nodes are considered "textual". TODO: Or at least not in TS because we can check if a node is the leaf node. Unsure if this assumption will hold for all other compilers +We should go X number of times up checking if `a.parent === b.parent` + +TODO: Think if we want to enable this at all stages of the match finder or at the end only + +## Changes compaction + +Needed to enable the tests + +## Move ignorer + +This is a pre-requisite for the alignment problem. Should be enabled/disabled by option + +This is important to enable tests ## Alignment Haven't planned how to implement this but this has to be for version 1.0 @@ -63,14 +86,11 @@ Also we need to consider the case where `startLine !== endLine` form example tag We should create a `recordStep` function that tracks the current "Thinking" for the algorithm. This should be visualized in the front end (or terminal) by clicking forward / backwards. This is going to be pretty important to debug the algo, especially useful in tricky scenarios -# Low prio +############################# Low prio ############################# ## Better organize code We should have the possibility to override functions to experiment with new behaviours, for example how we diff code either with the string-to-string method or the zigzag algo -## UNSURE | Use AST instead of flattened array -TODO: Think why this is needed. In any case, we can have the iterator abstract this away - ## Intra-node diffing In the current approach, we consider the following a change @@ -105,4 +125,17 @@ Maybe we have some business here ## Tree sitter & other languages Also having languages communicate via RPC or something, right now we are loading typescript in a JS module, this will prevent us from using a non-js compiler -Maybe here we can also conside sublanguages, an HTML file can have JS and CSS as well as HTML code \ No newline at end of file +Maybe here we can also conside sublanguages, an HTML file can have JS and CSS as well as HTML code + +## Plain text compiler +We could experimentate with being the differ of CRDT by writing a plain text parser that simply separest by white space, and structures the code per sentence / new line. + +## Another language supported + +###################### DONE ############# + +## Moves now contain sections +Before we would create a move saying, "From X to Y it moved to A and B, limiting us to a single section, this is why we had to include the closing node as a separate field. Also, this single-section approach will limit us considering the above point where now a move could be, for example: +"perfect match", "addition", "perfect match" + +So moves should have an array of sections \ No newline at end of file From 69a2aab8b53d89fbb8db235771094272d4974a81 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sat, 23 Mar 2024 21:25:43 -0300 Subject: [PATCH 43/79] Moved code around --- src/v2/core.ts | 35 +++++++++-------------------------- src/v2/diff.ts | 31 +++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/v2/core.ts b/src/v2/core.ts index 002861f..1b71be1 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -1,5 +1,5 @@ import { _context } from "."; -import { getBestMatch, getSubSequenceNodes } from "./diff"; +import { getBestMatch, getBestMatchFromSubsequenceNodes } from "./diff"; import { getIndexesFromSegment, sort } from "./utils"; import { Iterator } from "./iterator"; import { DiffType } from "../types"; @@ -13,14 +13,13 @@ export function computeDiff() { const a = iterA.next(); const b = iterB.next(); - // No more nodes to process. We are done if (!a && !b) { break; } - // One of the iterators ran out of nodes. We will mark the remaining unmatched nodes if (!a || !b) { - // If A finished means that B still have nodes, they are additions. If B finished means that A have nodes, they are deletions + // If A finished means that B still have nodes, report them as additions + // If B finished means that A still have nodes, report them as deletions const iterOn = !a ? iterB : iterA; const type = !a ? DiffType.addition : DiffType.deletion; @@ -28,8 +27,8 @@ export function computeDiff() { break; } - // 1- - const bestMatchForB = getBestMatch(b); + // 1- Get the best match for the current node + let bestMatchForB = getBestMatch(b); if (!bestMatchForB) { additions.push(b.getSegment(DiffType.addition)); @@ -38,27 +37,11 @@ export function computeDiff() { continue; } - // May be empty if the node we are looking for was the only one - const subSequenceNodesToCheck = getSubSequenceNodes(bestMatchForB, b); + // 2- Get all possible matches from subsequence nodes and compare it with the original match found. Pick the best + bestMatchForB = getBestMatchFromSubsequenceNodes(bestMatchForB, b); - let bestCandidate = bestMatchForB; - - for (const node of subSequenceNodesToCheck) { - const newCandidate = getBestMatch(node); - - if (!newCandidate) { - additions.push(b.getSegment(DiffType.addition)); - iterB.mark(b.index, DiffType.addition); - - continue; - } - - if (newCandidate.isBetterThan(bestCandidate)) { - bestCandidate = newCandidate; - } - } - - const move = Change.createMove(bestCandidate); + // 3- Store the match, mark nodes and continue + const move = Change.createMove(bestMatchForB); moves.push(move); markMatched(move); continue; diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 4d4f8b0..4f4eedc 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -7,6 +7,7 @@ import { equals, getAllNodesFromMatch, getIndexesFromSegment, getIterFromSide, g import { Segment } from "./types"; import { CandidateMatch } from "./match"; import { Side } from "../shared/language"; +import { DiffType } from "../types"; /** * Returns the longest possible match for the given node, this is including possible skips to improve the match length. @@ -36,7 +37,7 @@ export function getBestMatch(node: Node): CandidateMatch | undefined { return bestMatch; } -export function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { +function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { const nodesInMatch = getAllNodesFromMatch(match); const allNodes = new Set(); @@ -57,7 +58,7 @@ export function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { // SCORE FN PARAMETERS const MAX_NODE_SKIPS = 5; -export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { +function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); const segments: Segment[] = []; @@ -230,3 +231,29 @@ export function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { return new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); } + +export function getBestMatchFromSubsequenceNodes(currentBestMatch: CandidateMatch, b: Node) { + const { iterB, additions } = _context; + + // May be empty if the node we are looking for was the only one + const subSequenceNodesToCheck = getSubSequenceNodes(currentBestMatch, b); + + let bestSubsequenceMatch = currentBestMatch; + + for (const node of subSequenceNodesToCheck) { + const newCandidate = getBestMatch(node); + + if (!newCandidate) { + additions.push(b.getSegment(DiffType.addition)); + iterB.mark(b.index, DiffType.addition); + + continue; + } + + if (newCandidate.isBetterThan(bestSubsequenceMatch)) { + bestSubsequenceMatch = newCandidate; + } + } + + return bestSubsequenceMatch; +} From c8e95b424ee2537425b73d02bd2e014830e87f30 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sat, 23 Mar 2024 21:38:31 -0300 Subject: [PATCH 44/79] Cleaned up options code, merged into context --- src/v2/diff.ts | 5 ++--- src/v2/index.ts | 15 +++++---------- src/v2/types.ts | 1 + src/v2/utils.ts | 16 +++++++++++++--- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 4f4eedc..b5b1a16 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -55,12 +55,11 @@ function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { return [...allNodes]; } -// SCORE FN PARAMETERS -const MAX_NODE_SKIPS = 5; - function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); + const MAX_NODE_SKIPS = _context.options.maxNodeSkips; + const segments: Segment[] = []; const { iterA, iterB } = _context; diff --git a/src/v2/index.ts b/src/v2/index.ts index ed8f961..31bd811 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -2,7 +2,7 @@ import { Context } from "./utils"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; import { computeDiff } from "./core"; -import { asciiRenderFn, fail, prettyRenderFn } from "../debug"; +import { fail } from "../debug"; import { Options, OutputType, ResultTypeMapper, Segment } from "./types"; import { applyChangesToSources } from "./printer"; import { Change, Move } from "./change"; @@ -10,14 +10,7 @@ import { computeMoveAlignment } from "./semanticAligment"; import { compactAndCreateDiff } from "./compact"; import { DiffType } from "../types"; -const defaultOptions: Required = { - outputType: OutputType.changes, - tryAlignMoves: true, -}; - export function getDiff2<_OutputType extends OutputType = OutputType.changes>(sourceA: string, sourceB: string, options?: Options<_OutputType>): ResultTypeMapper[_OutputType] { - const _options = { ...defaultOptions, ...(options || {}) }; - const iterA = new Iterator(sourceA, Side.a); const iterB = new Iterator(sourceB, Side.b); @@ -26,7 +19,9 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so const deletions: Segment[] = []; const additions: Segment[] = []; - _context = new Context(sourceA, sourceB, iterA, iterB, moves, deletions, additions); + _context = new Context(options, sourceA, sourceB, iterA, iterB, moves, deletions, additions); + + const _options = _context.options; moves = computeDiff(); @@ -58,7 +53,7 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so return applyChangesToSources(sourceA, sourceB, changes, true) as ResultTypeMapper[_OutputType]; } default: - fail(); + fail("Unknown output type"); } } diff --git a/src/v2/types.ts b/src/v2/types.ts index 1c8fd1f..bdfdbcf 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -14,6 +14,7 @@ export interface ParsedProgram { export interface Options<_OutputType extends OutputType = OutputType.changes> { outputType?: _OutputType; tryAlignMoves?: boolean; + maxNodeSkips?: number; } export enum OutputType { diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 439baba..0a4335b 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -5,13 +5,21 @@ import { range } from "../utils"; import { Move } from "./change"; import { Iterator } from "./iterator"; import { Node } from "./node"; -import { Segment } from "./types"; +import { Options, OutputType, Segment } from "./types"; import { CandidateMatch } from "./match"; -export class Context { +const defaultOptions: Required = { + outputType: OutputType.changes, + tryAlignMoves: true, + maxNodeSkips: 5, +}; + +export class Context<_OutputType extends OutputType = OutputType.changes> { // Iterators will get stored once they are initialize, which happens later on the execution + options: any; constructor( + options: Options<_OutputType> | undefined, public sourceA: string, public sourceB: string, public iterA: Iterator, @@ -19,7 +27,9 @@ export class Context { public moves: Move[], public deletions: Segment[], public additions: Segment[], - ) {} + ) { + this.options = { ...defaultOptions, ...(options || {}) }; + } } export function equals(nodeOne: Node, nodeTwo: Node): boolean { From 59167cd98fa37baf8a8248f67395900f700c2839 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sat, 23 Mar 2024 21:38:43 -0300 Subject: [PATCH 45/79] some notes --- TODO.md | 16 ++++++++++++++++ readme.md | 9 +++++++++ v2.md | 5 ----- 3 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..66b4446 --- /dev/null +++ b/TODO.md @@ -0,0 +1,16 @@ +# Tasks: + +- Open close verifier +- Can node be matched alone +- Cheking parents to avoid semantic errors +- More draw functions + - Change + - Segment + - Semantic aignment table +- Update UI to work with V2 + +- !!! Merge v2 and remove old code + +- Alignment +- Pass configs (compiler, lang name, openClose, singleMatchingNode) as language options +- Fuzzer to detect crashes \ No newline at end of file diff --git a/readme.md b/readme.md index ad40d73..f3f4ada 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,14 @@ ## BetterDiff 🔀 +## WIP V2 + +Folder structure, everything currently being worked on is in the v2 folder + +- `index.ts` exposes the `getDiff2` function, the main entry point. Parses options, set up the context, gets the diff and outputs the changes in the desired format +- `core.ts` contains the main loop, getting the next nodes in line, finding matches, marking them as matched and moving on +- `diff.ts` contains the algorithm for the best match finder + + This is a _heavily experimental_ tool designed as an alternative to `git diff`, a program used to compare two text files a see the differences between them. The tool generates an output that details the differences between the files, which can be viewed either through the included frontend or as text by running it in the CLI. In contrast with `git diff` and other existing tools, this project offers: diff --git a/v2.md b/v2.md index e1c33e6..4ec6a71 100644 --- a/v2.md +++ b/v2.md @@ -1,8 +1,3 @@ -# Tasks: -- Open close verifier -- Can node be matched alone -- Pass configs (compiler, lang name, openClose, singleMatchingNode) as language options - This is a high-level overview of the second version of the algorithm. I'm reworking the whole system to solve the following issues that arise during the development of the previous version and, despite the efforts, I couldn't build the existing architecture to solve them. So I'm starting from scratch with those limitations in mind, these are: # High prio From da408731743815e8071b689fec4ee068eb3df9c0 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sat, 23 Mar 2024 21:38:54 -0300 Subject: [PATCH 46/79] enabled more tests --- package.json | 2 +- tests/conformance/move.test.ts | 30 +- tests/conformance/sequence-matching.test.ts | 433 ++++++++++---------- tests/conformance/trivia-diff.test.ts | 9 +- 4 files changed, 234 insertions(+), 240 deletions(-) diff --git a/package.json b/package.json index 349f8ba..1283bbc 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "test": "vitest -c ./vitest.config.js", - "testNew": "vitest -c ./vitest.config.js additions-removals changes move", + "testNew": "vitest -c ./vitest.config.js additions-removals changes move sequence-matching trivia-diff", "test-v2": "vitest -c ./vitest.config.js ./tests/v2", "build": "tsc --watch", "check": "npm run format && npm run lint", diff --git a/tests/conformance/move.test.ts b/tests/conformance/move.test.ts index 51bc28c..9cb7849 100644 --- a/tests/conformance/move.test.ts +++ b/tests/conformance/move.test.ts @@ -117,21 +117,21 @@ describe("Properly report lines added", () => { `, }); - test({ - name: "Mix of move with deletions and additions", - a: ` - console.log() && 3 - `, - b: ` - fn(console.log(2)) - `, - expA: ` - console.log() ➖&& 3➖ - `, - expB: ` - ➕fn(➕console.log(➕2➕)➕)➕ - `, - }); + // test({ + // name: "Mix of move with deletions and additions", + // a: ` + // console.log() && 3 + // `, + // b: ` + // fn(console.log(2)) + // `, + // expA: ` + // console.log() ➖&& 3➖ + // `, + // expB: ` + // ➕fn(➕console.log(➕2➕)➕)➕ + // `, + // }); // test({ // name: "Mix of move with deletions and additions 2", diff --git a/tests/conformance/sequence-matching.test.ts b/tests/conformance/sequence-matching.test.ts index 67270e7..10fd43a 100644 --- a/tests/conformance/sequence-matching.test.ts +++ b/tests/conformance/sequence-matching.test.ts @@ -1,4 +1,4 @@ -import { describe, test as vTest } from "vitest"; +import { describe } from "vitest"; import { test } from "../utils2"; describe("Properly report moves in a same sequence", () => { @@ -32,10 +32,6 @@ describe("Properly report moves in a same sequence", () => { expA: ` let age = 24 ➖&&➖ print('elian') `, - expB: ` - let age = 24 - print('elian') - `, }); test({ @@ -74,51 +70,49 @@ describe("Properly report moves in a same sequence", () => { `, }); - test({ - name: "Back and forth", - a: ` - let age = 24 && print('elian') - fn() - 1 - `, - b: ` - let age = 24 || fn() - print('elian') - `, - expA: ` - let age = 24 ➖&&➖ print('elian') - ⏩fn()⏪ - ➖1➖ - `, - expB: ` - let age = 24 ➕||➕ ⏩fn()⏪ - print('elian') - `, - }); + // test({ + // name: "Back and forth", + // a: ` + // let age = 24 && print('elian') + // fn() + // 1 + // `, + // b: ` + // let age = 24 || fn() + // print('elian') + // `, + // expA: ` + // let age = 24 ➖&&➖ print('elian') + // ⏩fn()⏪ + // ➖1➖ + // `, + // expB: ` + // let age = 24 ➕||➕ ⏩fn()⏪ + // print('elian') + // `, + // }); - // TODO: Can be improved - test({ - name: "Mid sequence", - a: ` - let up; - let middle; - `, - b: ` - let middle; - let down; - `, - expA: ` - ➖let up;➖ - let middle; - `, - expB: ` - let middle; - ➕let down;➕ - `, - }); -}); + // // TODO: Can be improved + // test({ + // name: "Mid sequence", + // a: ` + // let up; + // let middle; + // `, + // b: ` + // let middle; + // let down; + // `, + // expA: ` + // ➖let up;➖ + // let middle; + // `, + // expB: ` + // let middle; + // ➕let down;➕ + // `, + // }); -describe("Recursive matching", () => { test({ name: "Recursive matching 1", a: ` @@ -167,129 +161,129 @@ describe("Recursive matching", () => { `, }); - test({ - name: "Recursive matching 3", - a: ` - 12 - 12 34 - 12 34 56 - `, - b: ` - 12 34 56 - 0 - 12 - 0 - 0 - 12 34 - `, - expA: ` - ⏩12⏪ - ⏩12 34⏪ - 12 34 56 - `, - expB: ` - 12 34 56 - ➕0➕ - ⏩12⏪ - ➕0 - 0➕ - ⏩12 34⏪ - `, - }); + // test({ + // name: "Recursive matching 3", + // a: ` + // 12 + // 12 34 + // 12 34 56 + // `, + // b: ` + // 12 34 56 + // 0 + // 12 + // 0 + // 0 + // 12 34 + // `, + // expA: ` + // ⏩12⏪ + // ⏩12 34⏪ + // 12 34 56 + // `, + // expB: ` + // 12 34 56 + // ➕0➕ + // ⏩12⏪ + // ➕0 + // 0➕ + // ⏩12 34⏪ + // `, + // }); - test({ - name: "Recursive matching 4", - a: ` - 12 - 12 34 - 12 34 56 - `, - b: ` - 12 34 56 - 0 - 12 - 0 - 0 - 12 34 - `, - expA: ` - ⏩12⏪ - ⏩12 34⏪ - 12 34 56 - `, - expB: ` - 12 34 56 - ➕0➕ - ⏩12⏪ - ➕0 - 0➕ - ⏩12 34⏪ - `, - }); + // test({ + // name: "Recursive matching 4", + // a: ` + // 12 + // 12 34 + // 12 34 56 + // `, + // b: ` + // 12 34 56 + // 0 + // 12 + // 0 + // 0 + // 12 34 + // `, + // expA: ` + // ⏩12⏪ + // ⏩12 34⏪ + // 12 34 56 + // `, + // expB: ` + // 12 34 56 + // ➕0➕ + // ⏩12⏪ + // ➕0 + // 0➕ + // ⏩12 34⏪ + // `, + // }); - // This tests going backward in the LCS calculation - test({ - name: "Recursive matching 5", - a: ` - let start + // // This tests going backward in the LCS calculation + // test({ + // name: "Recursive matching 5", + // a: ` + // let start - export function bar(range) { - return { - start: range.start - }; - } - `, - b: ` - function foo() { } + // export function bar(range) { + // return { + // start: range.start + // }; + // } + // `, + // b: ` + // function foo() { } - export function bar(range) { - return { - start: range.start - }; - } - `, - expA: ` - ➖let start➖ + // export function bar(range) { + // return { + // start: range.start + // }; + // } + // `, + // expA: ` + // ➖let start➖ - export function bar(range) { - return { - start: range.start - }; - } - `, - expB: ` - ➕function foo() { }➕ + // export function bar(range) { + // return { + // start: range.start + // }; + // } + // `, + // expB: ` + // ➕function foo() { }➕ - export function bar(range) { - return { - start: range.start - }; - } - `, - }); + // export function bar(range) { + // return { + // start: range.start + // }; + // } + // `, + // }); - // This tests the subsequence matching - test({ - name: "Recursive matching 6", - a: ` - 1 - import { Y } from "./y"; - import { X } from "./x"; - `, - b: ` - 1 - import { X } from "./x"; - `, - expA: ` - 1 - ➖import { Y } from "./y";➖ - import { X } from "./x"; - `, - expB: ` - 1 - import { X } from "./x"; - `, - }); + // // This tests the subsequence matching + // test({ + // name: "Recursive matching 6", + // a: ` + // 1 + // import { Y } from "./y"; + // import { X } from "./x"; + // `, + // b: ` + // 1 + // import { X } from "./x"; + // `, + // expA: ` + // 1 + // ➖import { Y } from "./y";➖ + // import { X } from "./x"; + // `, + // expB: ` + // 1 + // import { X } from "./x"; + // `, + // }); test({ name: "Random 1", @@ -313,64 +307,63 @@ describe("Recursive matching", () => { `, }); - // The bug that we are testing here is if we have 2 moves, crossing each other and both are of the same length. The result - // is that depending of which one gets processed first, that will be aligned, this means that the result is not the same A to B and B to A, - // this is why I had to create the cases separated - test({ - only: "standard", - name: "Random 2 standard", - a: ` - 1 - 2 - 3 - 4 - `, - b: ` - 5 - 4 - 3 - `, - expA: ` - ➖1 - 2➖ - ⏩3⏪ - 4 - `, - expB: ` - ➕5➕ - 4 - ⏩3⏪ - `, - }); + // // The bug that we are testing here is if we have 2 moves, crossing each other and both are of the same length. The result + // // is that depending of which one gets processed first, that will be aligned, this means that the result is not the same A to B and B to A, + // // this is why I had to create the cases separated + // test({ + // only: "standard", + // name: "Random 2 standard", + // a: ` + // 1 + // 2 + // 3 + // 4 + // `, + // b: ` + // 5 + // 4 + // 3 + // `, + // expA: ` + // ➖1 + // 2➖ + // ⏩3⏪ + // 4 + // `, + // expB: ` + // ➕5➕ + // 4 + // ⏩3⏪ + // `, + // }); - test({ - only: "inversed", - name: "Random 2 inversed", - a: ` - 1 - 2 - 3 - 4 - `, - b: ` - 5 - 4 - 3 - `, - expA: ` - ➖1 - 2➖ - 3 - ⏩4⏪ - `, - expB: ` - ➕5➕ - ⏩4⏪ - 3 - `, - }); + // test({ + // only: "inversed", + // name: "Random 2 inversed", + // a: ` + // 1 + // 2 + // 3 + // 4 + // `, + // b: ` + // 5 + // 4 + // 3 + // `, + // expA: ` + // ➖1 + // 2➖ + // 3 + // ⏩4⏪ + // `, + // expB: ` + // ➕5➕ + // ⏩4⏪ + // 3 + // `, + // }); - // Used to crash when inserting new line alignments in "processMoves" when aligning two moves test({ name: "Random 3", a: ` diff --git a/tests/conformance/trivia-diff.test.ts b/tests/conformance/trivia-diff.test.ts index 13d375d..0725a7b 100644 --- a/tests/conformance/trivia-diff.test.ts +++ b/tests/conformance/trivia-diff.test.ts @@ -1,6 +1,7 @@ import { describe, test } from "vitest"; -import { getDiff } from "../../src"; +import { getDiff2 } from "../../src/v2"; import { validateDiff } from "../utils2"; +import { OutputType } from "../../src/v2/types"; describe("Ignore trivia", () => { test("Case 1", () => { @@ -13,7 +14,7 @@ describe("Ignore trivia", () => { const resultA = a; const resultB = b; - const { sourceA, sourceB } = getDiff(a, b); + const { sourceA, sourceB } = getDiff2(a, b, { outputType: OutputType.text }); validateDiff(a, b, resultA, resultB, sourceA, sourceB); }); @@ -28,7 +29,7 @@ describe("Ignore trivia", () => { const resultA = a; const resultB = b; - const { sourceA, sourceB } = getDiff(a, b); + const { sourceA, sourceB } = getDiff2(a, b, { outputType: OutputType.text }); validateDiff(a, b, resultA, resultB, sourceA, sourceB); }); @@ -45,7 +46,7 @@ describe("Ignore trivia", () => { const resultA = a; const resultB = b; - const { sourceA, sourceB } = getDiff(a, b); + const { sourceA, sourceB } = getDiff2(a, b, { outputType: OutputType.text }); validateDiff(a, b, resultA, resultB, sourceA, sourceB); }); From 98258b2482fa08c6346f56696e41fbbb848e0ccb Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Sat, 23 Mar 2024 21:44:28 -0300 Subject: [PATCH 47/79] updated vite --- package.json | 9 +- pnpm-lock.yaml | 751 +++++++++++++++++++++----- vitest.config.js => vitest.config.mjs | 0 3 files changed, 635 insertions(+), 125 deletions(-) rename vitest.config.js => vitest.config.mjs (100%) diff --git a/package.json b/package.json index 1283bbc..a232a03 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ }, "scripts": { "test": "vitest -c ./vitest.config.js", - "testNew": "vitest -c ./vitest.config.js additions-removals changes move sequence-matching trivia-diff", - "test-v2": "vitest -c ./vitest.config.js ./tests/v2", + "testNew": "vitest -c ./vitest.config.mjs additions-removals changes move sequence-matching trivia-diff", + "test-v2": "vitest -c ./vitest.config.mjs ./tests/v2", "build": "tsc --watch", "check": "npm run format && npm run lint", "format": "deno fmt", @@ -31,11 +31,12 @@ }, "devDependencies": { "@types/benchmark": "^2.1.2", + "@types/node": "^20.11.30", "benchmark": "^2.1.4", "cli-table3": "^0.6.3", "pprof-it": "^1.2.1", "tsx": "^4.7.0", "vite": "4.4.2", - "vitest": "0.33.0" + "vitest": "1.4.0" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a84157..e94b5ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,9 @@ devDependencies: '@types/benchmark': specifier: ^2.1.2 version: 2.1.2 + '@types/node': + specifier: ^20.11.30 + version: 20.11.30 benchmark: specifier: ^2.1.4 version: 2.1.4 @@ -36,10 +39,10 @@ devDependencies: version: 4.7.0 vite: specifier: 4.4.2 - version: 4.4.2(@types/node@20.4.1) + version: 4.4.2(@types/node@20.11.30) vitest: - specifier: 0.33.0 - version: 0.33.0 + specifier: 1.4.0 + version: 1.4.0(@types/node@20.11.30) packages: @@ -73,6 +76,15 @@ packages: dev: true optional: true + /@esbuild/aix-ppc64@0.20.2: + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.18.11: resolution: {integrity: sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==} engines: {node: '>=12'} @@ -91,6 +103,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.20.2: + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.18.11: resolution: {integrity: sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==} engines: {node: '>=12'} @@ -109,6 +130,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.20.2: + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.18.11: resolution: {integrity: sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==} engines: {node: '>=12'} @@ -127,6 +157,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.20.2: + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.18.11: resolution: {integrity: sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==} engines: {node: '>=12'} @@ -145,6 +184,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.20.2: + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.18.11: resolution: {integrity: sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==} engines: {node: '>=12'} @@ -163,6 +211,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.20.2: + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.18.11: resolution: {integrity: sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==} engines: {node: '>=12'} @@ -181,6 +238,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.20.2: + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.18.11: resolution: {integrity: sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==} engines: {node: '>=12'} @@ -199,6 +265,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.20.2: + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.18.11: resolution: {integrity: sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==} engines: {node: '>=12'} @@ -217,6 +292,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.20.2: + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.18.11: resolution: {integrity: sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==} engines: {node: '>=12'} @@ -235,6 +319,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.20.2: + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.18.11: resolution: {integrity: sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==} engines: {node: '>=12'} @@ -253,6 +346,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.20.2: + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.18.11: resolution: {integrity: sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==} engines: {node: '>=12'} @@ -271,6 +373,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.20.2: + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.18.11: resolution: {integrity: sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==} engines: {node: '>=12'} @@ -289,6 +400,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.20.2: + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.18.11: resolution: {integrity: sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==} engines: {node: '>=12'} @@ -307,6 +427,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.20.2: + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.18.11: resolution: {integrity: sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==} engines: {node: '>=12'} @@ -325,6 +454,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.20.2: + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.18.11: resolution: {integrity: sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==} engines: {node: '>=12'} @@ -343,6 +481,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.20.2: + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.18.11: resolution: {integrity: sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==} engines: {node: '>=12'} @@ -361,6 +508,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.20.2: + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.18.11: resolution: {integrity: sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==} engines: {node: '>=12'} @@ -379,6 +535,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.20.2: + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.18.11: resolution: {integrity: sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==} engines: {node: '>=12'} @@ -397,6 +562,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.20.2: + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.18.11: resolution: {integrity: sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==} engines: {node: '>=12'} @@ -415,6 +589,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.20.2: + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.18.11: resolution: {integrity: sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==} engines: {node: '>=12'} @@ -433,6 +616,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.20.2: + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.18.11: resolution: {integrity: sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==} engines: {node: '>=12'} @@ -451,6 +643,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.20.2: + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.18.11: resolution: {integrity: sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==} engines: {node: '>=12'} @@ -469,6 +670,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.20.2: + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@fastify/ajv-compiler@3.5.0: resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} dependencies: @@ -498,8 +708,8 @@ packages: fast-json-stringify: 5.7.0 dev: false - /@jest/schemas@29.6.0: - resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@sinclair/typebox': 0.27.8 @@ -509,6 +719,110 @@ packages: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true + /@rollup/rollup-android-arm-eabi@4.13.0: + resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.13.0: + resolution: {integrity: sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.13.0: + resolution: {integrity: sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.13.0: + resolution: {integrity: sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.13.0: + resolution: {integrity: sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.13.0: + resolution: {integrity: sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.13.0: + resolution: {integrity: sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.13.0: + resolution: {integrity: sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.13.0: + resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.13.0: + resolution: {integrity: sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.13.0: + resolution: {integrity: sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.13.0: + resolution: {integrity: sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.13.0: + resolution: {integrity: sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -517,56 +831,53 @@ packages: resolution: {integrity: sha512-EDKtLYNMKrig22jEvhXq8TBFyFgVNSPmDF2b9UzJ7+eylPqdZVo17PCUMkn1jP6/1A/0u78VqYC6VrX6b8pDWA==} dev: true - /@types/chai-subset@1.3.3: - resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} - dependencies: - '@types/chai': 4.3.5 - dev: true - - /@types/chai@4.3.5: - resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/node@20.4.1: - resolution: {integrity: sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==} + /@types/node@20.11.30: + resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} + dependencies: + undici-types: 5.26.5 dev: true - /@vitest/expect@0.33.0: - resolution: {integrity: sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==} + /@vitest/expect@1.4.0: + resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} dependencies: - '@vitest/spy': 0.33.0 - '@vitest/utils': 0.33.0 - chai: 4.3.7 + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + chai: 4.4.1 dev: true - /@vitest/runner@0.33.0: - resolution: {integrity: sha512-UPfACnmCB6HKRHTlcgCoBh6ppl6fDn+J/xR8dTufWiKt/74Y9bHci5CKB8tESSV82zKYtkBJo9whU3mNvfaisg==} + /@vitest/runner@1.4.0: + resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} dependencies: - '@vitest/utils': 0.33.0 - p-limit: 4.0.0 + '@vitest/utils': 1.4.0 + p-limit: 5.0.0 pathe: 1.1.1 dev: true - /@vitest/snapshot@0.33.0: - resolution: {integrity: sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==} + /@vitest/snapshot@1.4.0: + resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} dependencies: - magic-string: 0.30.1 + magic-string: 0.30.8 pathe: 1.1.1 - pretty-format: 29.6.1 + pretty-format: 29.7.0 dev: true - /@vitest/spy@0.33.0: - resolution: {integrity: sha512-Kv+yZ4hnH1WdiAkPUQTpRxW8kGtH8VRTnus7ZTGovFYM1ZezJpvGtb9nPIjPnptHbsyIAxYZsEpVPYgtpjGnrg==} + /@vitest/spy@1.4.0: + resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} dependencies: - tinyspy: 2.1.1 + tinyspy: 2.2.1 dev: true - /@vitest/utils@0.33.0: - resolution: {integrity: sha512-pF1w22ic965sv+EN6uoePkAOTkAPWM03Ri/jXNyMIKBb/XHLDPfhLvf/Fa9g0YECevAIz56oVYXhodLvLQ/awA==} + /@vitest/utils@1.4.0: + resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} dependencies: - diff-sequences: 29.4.3 - loupe: 2.3.6 - pretty-format: 29.6.1 + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 dev: true /abort-controller@3.0.0: @@ -580,13 +891,13 @@ packages: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} dev: false - /acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} dev: true - /acorn@8.10.0: - resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} hasBin: true dev: true @@ -667,21 +978,23 @@ packages: engines: {node: '>=8'} dev: true - /chai@4.3.7: - resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} engines: {node: '>=4'} dependencies: assertion-error: 1.1.0 - check-error: 1.0.2 + check-error: 1.0.3 deep-eql: 4.1.3 - get-func-name: 2.0.0 + get-func-name: 2.0.2 loupe: 2.3.6 pathval: 1.1.1 type-detect: 4.0.8 dev: true - /check-error@1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 dev: true /cli-table3@0.6.3: @@ -730,8 +1043,8 @@ packages: engines: {node: '>=10'} dev: true - /diff-sequences@29.4.3: - resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true @@ -800,6 +1113,43 @@ packages: '@esbuild/win32-x64': 0.19.12 dev: true + /esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + dev: true + + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -810,6 +1160,21 @@ packages: engines: {node: '>=0.8.x'} dev: false + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + /fast-content-type-parse@1.0.0: resolution: {integrity: sha512-Xbc4XcysUXcsP5aHUU7Nq3OwvHq97C+WnbkeIefpeYLX+ryzFJlU6OStFJhs6Ol0LkUGpcK+wL0JwfM+FCU5IA==} dev: false @@ -911,8 +1276,13 @@ packages: dev: true optional: true - /get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} dev: true /get-tsconfig@4.7.2: @@ -921,6 +1291,11 @@ packages: resolve-pkg-maps: 1.0.0 dev: true + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false @@ -935,10 +1310,19 @@ packages: engines: {node: '>=8'} dev: true + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /js-tokens@8.0.3: + resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + dev: true + /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} dev: false @@ -960,9 +1344,12 @@ packages: set-cookie-parser: 2.6.0 dev: false - /local-pkg@0.4.3: - resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} + dependencies: + mlly: 1.6.1 + pkg-types: 1.0.3 dev: true /lodash@4.17.21: @@ -971,8 +1358,15 @@ packages: /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + deprecated: Please upgrade to 2.3.7 which fixes GHSA-4q6p-r6v2-jvc5 dependencies: - get-func-name: 2.0.0 + get-func-name: 2.0.2 + dev: true + + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 dev: true /lru-cache@6.0.0: @@ -982,20 +1376,29 @@ packages: yallist: 4.0.0 dev: false - /magic-string@0.30.1: - resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /mlly@1.4.0: - resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /mlly@1.6.1: + resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} dependencies: - acorn: 8.10.0 - pathe: 1.1.1 + acorn: 8.11.3 + pathe: 1.1.2 pkg-types: 1.0.3 - ufo: 1.1.2 + ufo: 1.5.3 dev: true /mnemonist@0.39.5: @@ -1013,11 +1416,24 @@ packages: hasBin: true dev: true + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /node-gyp-build@3.9.0: resolution: {integrity: sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==} hasBin: true dev: true + /npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + /obliterator@2.0.4: resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} dev: false @@ -1026,6 +1442,13 @@ packages: resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} dev: false + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1033,9 +1456,9 @@ packages: yocto-queue: 0.1.0 dev: true - /p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} dependencies: yocto-queue: 1.0.0 dev: true @@ -1045,10 +1468,19 @@ packages: engines: {node: '>=8'} dev: true + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + /pathe@1.1.1: resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} dev: true + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + /pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} dev: true @@ -1094,7 +1526,7 @@ packages: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} dependencies: jsonc-parser: 3.2.0 - mlly: 1.4.0 + mlly: 1.6.1 pathe: 1.1.1 dev: true @@ -1111,6 +1543,15 @@ packages: source-map-js: 1.0.2 dev: true + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + dev: true + /pprof-format@2.0.7: resolution: {integrity: sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA==} dev: true @@ -1126,11 +1567,11 @@ packages: signal-exit: 3.0.7 dev: true - /pretty-format@29.6.1: - resolution: {integrity: sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==} + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.6.0 + '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 dev: true @@ -1212,6 +1653,29 @@ packages: fsevents: 2.3.3 dev: true + /rollup@4.13.0: + resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.13.0 + '@rollup/rollup-android-arm64': 4.13.0 + '@rollup/rollup-darwin-arm64': 4.13.0 + '@rollup/rollup-darwin-x64': 4.13.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.13.0 + '@rollup/rollup-linux-arm64-gnu': 4.13.0 + '@rollup/rollup-linux-arm64-musl': 4.13.0 + '@rollup/rollup-linux-riscv64-gnu': 4.13.0 + '@rollup/rollup-linux-x64-gnu': 4.13.0 + '@rollup/rollup-linux-x64-musl': 4.13.0 + '@rollup/rollup-win32-arm64-msvc': 4.13.0 + '@rollup/rollup-win32-ia32-msvc': 4.13.0 + '@rollup/rollup-win32-x64-msvc': 4.13.0 + fsevents: 2.3.3 + dev: true + /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: false @@ -1263,6 +1727,11 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /sonic-boom@3.3.0: resolution: {integrity: sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==} dependencies: @@ -1274,6 +1743,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: true + /source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} @@ -1294,8 +1768,8 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true - /std-env@3.3.3: - resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true /string-width@4.2.3: @@ -1320,10 +1794,15 @@ packages: ansi-regex: 5.0.1 dev: true - /strip-literal@1.0.1: - resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /strip-literal@2.0.0: + resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} dependencies: - acorn: 8.10.0 + js-tokens: 8.0.3 dev: true /thread-stream@2.3.0: @@ -1341,17 +1820,17 @@ packages: engines: {node: '>=12'} dev: false - /tinybench@2.5.0: - resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} dev: true - /tinypool@0.6.0: - resolution: {integrity: sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ==} + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} engines: {node: '>=14.0.0'} dev: true - /tinyspy@2.1.1: - resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} dev: true @@ -1377,8 +1856,12 @@ packages: hasBin: true dev: false - /ufo@1.1.2: - resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} + /ufo@1.5.3: + resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true /uri-js@4.4.1: @@ -1387,17 +1870,16 @@ packages: punycode: 2.3.0 dev: false - /vite-node@0.33.0(@types/node@20.4.1): - resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==} - engines: {node: '>=v14.18.0'} + /vite-node@1.4.0(@types/node@20.11.30): + resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: cac: 6.7.14 debug: 4.3.4 - mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.4.2(@types/node@20.4.1) + vite: 5.2.4(@types/node@20.11.30) transitivePeerDependencies: - '@types/node' - less @@ -1409,7 +1891,7 @@ packages: - terser dev: true - /vite@4.4.2(@types/node@20.4.1): + /vite@4.4.2(@types/node@20.11.30): resolution: {integrity: sha512-zUcsJN+UvdSyHhYa277UHhiJ3iq4hUBwHavOpsNUGsTgjBeoBlK8eDt+iT09pBq0h9/knhG/SPrZiM7cGmg7NA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -1437,7 +1919,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.4.1 + '@types/node': 20.11.30 esbuild: 0.18.11 postcss: 8.4.25 rollup: 3.26.2 @@ -1445,22 +1927,58 @@ packages: fsevents: 2.3.3 dev: true - /vitest@0.33.0: - resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==} - engines: {node: '>=v14.18.0'} + /vite@5.2.4(@types/node@20.11.30): + resolution: {integrity: sha512-vjFghvHWidBTinu5TCymJk/lRHlR5ljqB83yugr0HA1xspUPdOZHqbqDLnZ8f9/jINrtFHTCYYyIUi+o+Q5iyg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.30 + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@1.4.0(@types/node@20.11.30): + resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@vitest/browser': '*' - '@vitest/ui': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.4.0 + '@vitest/ui': 1.4.0 happy-dom: '*' jsdom: '*' - playwright: '*' - safaridriver: '*' - webdriverio: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true + '@types/node': + optional: true '@vitest/browser': optional: true '@vitest/ui': @@ -1469,36 +1987,27 @@ packages: optional: true jsdom: optional: true - playwright: - optional: true - safaridriver: - optional: true - webdriverio: - optional: true dependencies: - '@types/chai': 4.3.5 - '@types/chai-subset': 1.3.3 - '@types/node': 20.4.1 - '@vitest/expect': 0.33.0 - '@vitest/runner': 0.33.0 - '@vitest/snapshot': 0.33.0 - '@vitest/spy': 0.33.0 - '@vitest/utils': 0.33.0 - acorn: 8.10.0 - acorn-walk: 8.2.0 - cac: 6.7.14 - chai: 4.3.7 + '@types/node': 20.11.30 + '@vitest/expect': 1.4.0 + '@vitest/runner': 1.4.0 + '@vitest/snapshot': 1.4.0 + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + acorn-walk: 8.3.2 + chai: 4.4.1 debug: 4.3.4 - local-pkg: 0.4.3 - magic-string: 0.30.1 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.8 pathe: 1.1.1 picocolors: 1.0.0 - std-env: 3.3.3 - strip-literal: 1.0.1 - tinybench: 2.5.0 - tinypool: 0.6.0 - vite: 4.4.2(@types/node@20.4.1) - vite-node: 0.33.0(@types/node@20.4.1) + std-env: 3.7.0 + strip-literal: 2.0.0 + tinybench: 2.6.0 + tinypool: 0.8.2 + vite: 5.2.4(@types/node@20.11.30) + vite-node: 1.4.0(@types/node@20.11.30) why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/vitest.config.js b/vitest.config.mjs similarity index 100% rename from vitest.config.js rename to vitest.config.mjs From a82a67da8a83166e7a7a7149569b3d26c2337b4d Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 25 Mar 2024 17:21:19 -0300 Subject: [PATCH 48/79] Initial single matching node check --- src/v2/compilers.ts | 10 ++++++++++ src/v2/core.ts | 29 +++++++++++++++++++++++++++-- src/v2/index.ts | 11 ++++++++--- src/v2/types.ts | 5 +++++ src/v2/utils.ts | 20 +++++++++++++++++--- 5 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/v2/compilers.ts b/src/v2/compilers.ts index 8ab2b50..66263c6 100644 --- a/src/v2/compilers.ts +++ b/src/v2/compilers.ts @@ -81,3 +81,13 @@ function storeNodeInNodeTable(nodesTable: NodesTable, node: Node) { nodesTable.set(node.kind, [node]); } } + +export function canNodeBeMatchedAlone(node: TsNode) { + const kind = node.kind; + const isLiteral = kind >= SyntaxKind.FirstLiteralToken && kind <= SyntaxKind.LastLiteralToken; + const isIdentifier = kind === SyntaxKind.Identifier || kind === SyntaxKind.PrivateIdentifier; + const isTemplateString = kind === SyntaxKind.FirstTemplateToken || kind === SyntaxKind.LastTemplateToken; + const other = kind === SyntaxKind.DebuggerKeyword; + + return (isLiteral || isIdentifier || isTemplateString || other) && ((node as any).text || '').length >= 1; +} diff --git a/src/v2/core.ts b/src/v2/core.ts index 1b71be1..53536d2 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -1,13 +1,14 @@ import { _context } from "."; import { getBestMatch, getBestMatchFromSubsequenceNodes } from "./diff"; -import { getIndexesFromSegment, sort } from "./utils"; +import { getIndexesFromSegment, matchContainsSingleNode, sort } from "./utils"; import { Iterator } from "./iterator"; import { DiffType } from "../types"; import { Change } from "./change"; import { range } from "../utils"; +import { CandidateMatch } from "./match"; export function computeDiff() { - const { iterA, iterB, moves, additions } = _context; + const { iterA, iterB, moves, additions, deletions } = _context; while (true) { const a = iterA.next(); @@ -40,6 +41,23 @@ export function computeDiff() { // 2- Get all possible matches from subsequence nodes and compare it with the original match found. Pick the best bestMatchForB = getBestMatchFromSubsequenceNodes(bestMatchForB, b); + // 2.b + if (matchContainsSingleNode(bestMatchForB)) { + if (!checkIfNodeCanBeMatchedAlone(bestMatchForB)) { + const nodeA = iterA.nodes[bestMatchForB.segments[0][0]]; + const nodeB = iterB.nodes[bestMatchForB.segments[0][1]]; + + deletions.push(nodeA.getSegment(DiffType.deletion)); + additions.push(nodeB.getSegment(DiffType.addition)); + + iterA.mark(nodeA.index, DiffType.deletion); + iterB.mark(nodeB.index, DiffType.addition); + + continue; + } + } + + // 3- Store the match, mark nodes and continue const move = Change.createMove(bestMatchForB); moves.push(move); @@ -82,3 +100,10 @@ export function oneSidedIteration( node = iter.next(node.index + 1); } } + +function checkIfNodeCanBeMatchedAlone(match: CandidateMatch) { + const nodeIndex = match.segments[0][0]; + const node = _context.iterA.nodes[nodeIndex]; + + return _context.languageConfig.singleNodeMatchingFn(node) +} \ No newline at end of file diff --git a/src/v2/index.ts b/src/v2/index.ts index 31bd811..13a1e1f 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -3,14 +3,19 @@ import { Side } from "../shared/language"; import { Iterator } from "./iterator"; import { computeDiff } from "./core"; import { fail } from "../debug"; -import { Options, OutputType, ResultTypeMapper, Segment } from "./types"; +import { LanguageConfigs, Options, OutputType, ResultTypeMapper, Segment } from "./types"; import { applyChangesToSources } from "./printer"; import { Change, Move } from "./change"; import { computeMoveAlignment } from "./semanticAligment"; import { compactAndCreateDiff } from "./compact"; import { DiffType } from "../types"; -export function getDiff2<_OutputType extends OutputType = OutputType.changes>(sourceA: string, sourceB: string, options?: Options<_OutputType>): ResultTypeMapper[_OutputType] { +export function getDiff2<_OutputType extends OutputType = OutputType.changes>( + sourceA: string, + sourceB: string, + options?: Options<_OutputType>, + LanguageConfigs?: Partial +): ResultTypeMapper[_OutputType] { const iterA = new Iterator(sourceA, Side.a); const iterB = new Iterator(sourceB, Side.b); @@ -19,7 +24,7 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>(so const deletions: Segment[] = []; const additions: Segment[] = []; - _context = new Context(options, sourceA, sourceB, iterA, iterB, moves, deletions, additions); + _context = new Context(LanguageConfigs, options, sourceA, sourceB, iterA, iterB, moves, deletions, additions); const _options = _context.options; diff --git a/src/v2/types.ts b/src/v2/types.ts index bdfdbcf..28e1168 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -11,6 +11,11 @@ export interface ParsedProgram { nodesTable: NodesTable; } +export interface LanguageConfigs { + language: string; + singleNodeMatchingFn: (node: any) => boolean; +} + export interface Options<_OutputType extends OutputType = OutputType.changes> { outputType?: _OutputType; tryAlignMoves?: boolean; diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 0a4335b..14667ab 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -5,8 +5,14 @@ import { range } from "../utils"; import { Move } from "./change"; import { Iterator } from "./iterator"; import { Node } from "./node"; -import { Options, OutputType, Segment } from "./types"; +import { LanguageConfigs, Options, OutputType, Segment } from "./types"; import { CandidateMatch } from "./match"; +import { canNodeBeMatchedAlone } from "./compilers"; + +const defaultLanguageConfigs: LanguageConfigs = { + language: 'typescript', + singleNodeMatchingFn: canNodeBeMatchedAlone +} const defaultOptions: Required = { outputType: OutputType.changes, @@ -17,8 +23,11 @@ const defaultOptions: Required = { export class Context<_OutputType extends OutputType = OutputType.changes> { // Iterators will get stored once they are initialize, which happens later on the execution - options: any; + languageConfig: LanguageConfigs; + options: Required>; + constructor( + languageConfig: Partial | undefined, options: Options<_OutputType> | undefined, public sourceA: string, public sourceB: string, @@ -28,6 +37,7 @@ export class Context<_OutputType extends OutputType = OutputType.changes> { public deletions: Segment[], public additions: Segment[], ) { + this.languageConfig = { ...defaultLanguageConfigs, ...(languageConfig || {}) }; this.options = { ...defaultOptions, ...(options || {}) }; } } @@ -63,7 +73,6 @@ export function getTextLengthFromSegments(segments: Segment[]) { export function getAllNodesFromMatch(match: CandidateMatch, side = Side.b) { const iter = getIterFromSide(side); - const readFrom = side === Side.a ? 0 : 1; const nodes: Node[] = []; for (const segment of match.segments) { @@ -134,3 +143,8 @@ export const sort = { asc, desc, }; + + +export function matchContainsSingleNode(match: CandidateMatch) { + return match.segments.length === 1 && match.segments[0][2] === 1; +} \ No newline at end of file From 36b1c70f0a67473295887071ca3975bbd34ffb53 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 26 Mar 2024 12:53:02 -0300 Subject: [PATCH 49/79] Working skipping matches based on the single node logic --- src/v2/compilers.ts | 2 +- src/v2/core.ts | 42 ++++++++++++--------- src/v2/index.ts | 14 ++++--- src/v2/semanticAligment.ts | 1 + src/v2/types.ts | 2 +- src/v2/utils.ts | 9 ++--- tests/conformance/changes.test.ts | 5 ++- tests/conformance/sequence-matching.test.ts | 38 +++++++++---------- 8 files changed, 62 insertions(+), 51 deletions(-) diff --git a/src/v2/compilers.ts b/src/v2/compilers.ts index 66263c6..ddc1499 100644 --- a/src/v2/compilers.ts +++ b/src/v2/compilers.ts @@ -89,5 +89,5 @@ export function canNodeBeMatchedAlone(node: TsNode) { const isTemplateString = kind === SyntaxKind.FirstTemplateToken || kind === SyntaxKind.LastTemplateToken; const other = kind === SyntaxKind.DebuggerKeyword; - return (isLiteral || isIdentifier || isTemplateString || other) && ((node as any).text || '').length >= 1; + return (isLiteral || isIdentifier || isTemplateString || other) && ((node as any).text || "").length >= 1; } diff --git a/src/v2/core.ts b/src/v2/core.ts index 53536d2..3cf1d37 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -1,11 +1,11 @@ import { _context } from "."; import { getBestMatch, getBestMatchFromSubsequenceNodes } from "./diff"; -import { getIndexesFromSegment, matchContainsSingleNode, sort } from "./utils"; +import { getIndexesFromSegment, getTextLengthFromSegments, matchContainsSingleNode, sort } from "./utils"; import { Iterator } from "./iterator"; import { DiffType } from "../types"; import { Change } from "./change"; import { range } from "../utils"; -import { CandidateMatch } from "./match"; +import { Segment } from "./types"; export function computeDiff() { const { iterA, iterB, moves, additions, deletions } = _context; @@ -41,22 +41,30 @@ export function computeDiff() { // 2- Get all possible matches from subsequence nodes and compare it with the original match found. Pick the best bestMatchForB = getBestMatchFromSubsequenceNodes(bestMatchForB, b); - // 2.b - if (matchContainsSingleNode(bestMatchForB)) { - if (!checkIfNodeCanBeMatchedAlone(bestMatchForB)) { - const nodeA = iterA.nodes[bestMatchForB.segments[0][0]]; - const nodeB = iterB.nodes[bestMatchForB.segments[0][1]]; + // 2.b + const remainingSegments = []; + for (const segment of bestMatchForB.segments) { + if (segment[2] === 1 && !checkIfNodeCanBeMatchedAlone(segment)) { + const nodeA = iterA.nodes[segment[0]]; + const nodeB = iterB.nodes[segment[1]]; - deletions.push(nodeA.getSegment(DiffType.deletion)); - additions.push(nodeB.getSegment(DiffType.addition)); + deletions.push(nodeA.getSegment(DiffType.deletion)); + additions.push(nodeB.getSegment(DiffType.addition)); - iterA.mark(nodeA.index, DiffType.deletion); - iterB.mark(nodeB.index, DiffType.addition); - - continue; + iterA.mark(nodeA.index, DiffType.deletion); + iterB.mark(nodeB.index, DiffType.addition); + } else { + remainingSegments.push(segment); } } + if (remainingSegments.length === 0) { + continue; + } else if (remainingSegments.length !== bestMatchForB.segments.length) { + // TODO-2: Update skips as well + bestMatchForB.segments = remainingSegments; + bestMatchForB.textLength = getTextLengthFromSegments(remainingSegments); + } // 3- Store the match, mark nodes and continue const move = Change.createMove(bestMatchForB); @@ -101,9 +109,9 @@ export function oneSidedIteration( } } -function checkIfNodeCanBeMatchedAlone(match: CandidateMatch) { - const nodeIndex = match.segments[0][0]; +function checkIfNodeCanBeMatchedAlone(segment: Segment) { + const nodeIndex = segment[0]; const node = _context.iterA.nodes[nodeIndex]; - return _context.languageConfig.singleNodeMatchingFn(node) -} \ No newline at end of file + return _context.languageConfig.singleNodeMatchingFn(node); +} diff --git a/src/v2/index.ts b/src/v2/index.ts index 13a1e1f..403673d 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,4 +1,4 @@ -import { Context } from "./utils"; +import { Context, sort } from "./utils"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; import { computeDiff } from "./core"; @@ -11,10 +11,10 @@ import { compactAndCreateDiff } from "./compact"; import { DiffType } from "../types"; export function getDiff2<_OutputType extends OutputType = OutputType.changes>( - sourceA: string, - sourceB: string, + sourceA: string, + sourceB: string, options?: Options<_OutputType>, - LanguageConfigs?: Partial + LanguageConfigs?: Partial, ): ResultTypeMapper[_OutputType] { const iterA = new Iterator(sourceA, Side.a); const iterB = new Iterator(sourceB, Side.b); @@ -38,12 +38,14 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>( const changes: Change[] = moves; if (_context.additions.length) { - const additionsChange = compactAndCreateDiff(DiffType.addition, additions); + // Sort before compacting + const additionsChange = compactAndCreateDiff(DiffType.addition, additions.sort((a, b) => sort.asc(a[1], b[1]))); changes.push(additionsChange); } if (_context.deletions.length) { - const deletionsChange = compactAndCreateDiff(DiffType.deletion, deletions); + // Sort before compacting + const deletionsChange = compactAndCreateDiff(DiffType.deletion, deletions.sort((a, b) => sort.asc(a[0], b[0]))); changes.push(deletionsChange); } diff --git a/src/v2/semanticAligment.ts b/src/v2/semanticAligment.ts index 748f4a2..7b36eae 100644 --- a/src/v2/semanticAligment.ts +++ b/src/v2/semanticAligment.ts @@ -43,6 +43,7 @@ export function computeMoveAlignment(changes: Move[]): Move[] { continue; } else { if (unalignedSegments.length !== change.textLength) { + // TODO-2: Update skips as well change.segments = unalignedSegments; change.textLength = getTextLengthFromSegments(unalignedSegments); } diff --git a/src/v2/types.ts b/src/v2/types.ts index 28e1168..5756ffa 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -13,7 +13,7 @@ export interface ParsedProgram { export interface LanguageConfigs { language: string; - singleNodeMatchingFn: (node: any) => boolean; + singleNodeMatchingFn: (node: any) => boolean; } export interface Options<_OutputType extends OutputType = OutputType.changes> { diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 14667ab..f065a39 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -10,9 +10,9 @@ import { CandidateMatch } from "./match"; import { canNodeBeMatchedAlone } from "./compilers"; const defaultLanguageConfigs: LanguageConfigs = { - language: 'typescript', - singleNodeMatchingFn: canNodeBeMatchedAlone -} + language: "typescript", + singleNodeMatchingFn: canNodeBeMatchedAlone, +}; const defaultOptions: Required = { outputType: OutputType.changes, @@ -144,7 +144,6 @@ export const sort = { desc, }; - export function matchContainsSingleNode(match: CandidateMatch) { return match.segments.length === 1 && match.segments[0][2] === 1; -} \ No newline at end of file +} diff --git a/tests/conformance/changes.test.ts b/tests/conformance/changes.test.ts index fc8b46f..ac03aed 100644 --- a/tests/conformance/changes.test.ts +++ b/tests/conformance/changes.test.ts @@ -82,6 +82,7 @@ describe("Properly report line changed", () => { `, }); + // TODO(Future): Once we implement changes, we will be able to narrow down this diff test({ name: "Multi line change", a: ` @@ -91,10 +92,10 @@ describe("Properly report line changed", () => { let firstName = "fernando" `, expA: ` - let ➖name➖ = ➖"elian"➖ + ➖let name = "elian"➖ `, expB: ` - let ➕firstName➕ = ➕"fernando"➕ + ➕let firstName = "fernando"➕ `, }); }); diff --git a/tests/conformance/sequence-matching.test.ts b/tests/conformance/sequence-matching.test.ts index 10fd43a..0a39607 100644 --- a/tests/conformance/sequence-matching.test.ts +++ b/tests/conformance/sequence-matching.test.ts @@ -93,25 +93,25 @@ describe("Properly report moves in a same sequence", () => { // }); // // TODO: Can be improved - // test({ - // name: "Mid sequence", - // a: ` - // let up; - // let middle; - // `, - // b: ` - // let middle; - // let down; - // `, - // expA: ` - // ➖let up;➖ - // let middle; - // `, - // expB: ` - // let middle; - // ➕let down;➕ - // `, - // }); + test({ + name: "Mid sequence", + a: ` + let up; + let middle; + `, + b: ` + let middle; + let down; + `, + expA: ` + ➖let up;➖ + let middle; + `, + expB: ` + let middle; + ➕let down;➕ + `, + }); test({ name: "Recursive matching 1", From 8e544931294fab0cdb89d0fe2b81ab37d9ec059e Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 26 Mar 2024 18:02:04 -0300 Subject: [PATCH 50/79] Cleanup --- src/v2/compact.ts | 27 ++++++++++++++++++++++++++- src/v2/index.ts | 22 ++++------------------ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/v2/compact.ts b/src/v2/compact.ts index 3fc1481..f964826 100644 --- a/src/v2/compact.ts +++ b/src/v2/compact.ts @@ -1,9 +1,34 @@ +import { _context } from "."; import { assert } from "../debug"; import { DiffType } from "../types"; import { Change } from "./change"; import { Segment } from "./types"; +import { sort } from "./utils"; -export function compactAndCreateDiff( +// We compact all tracked additions and deletions into a single change with multiple segments, we also compact them if possible +export function getAdditionsAndDeletionsChanges() { + const changes: Change[] = []; + + // Since compaction works by checking consecutive indexes we need to sort the segments + const sortedAdditions = _context.additions.sort((a, b) => sort.asc(a[1], b[1])); + const sortedDeletions = _context.deletions.sort((a, b) => sort.asc(a[0], b[0])); + + if (_context.additions.length) { + // Sort before compacting + const additionsChange = compactAndCreateDiff(DiffType.addition, sortedAdditions); + changes.push(additionsChange); + } + + if (_context.deletions.length) { + // Sort before compacting + const deletionsChange = compactAndCreateDiff(DiffType.deletion, sortedDeletions); + changes.push(deletionsChange); + } + + return changes; +} + +function compactAndCreateDiff( diffType: DiffType.addition | DiffType.deletion, segments: Segment[], ) { diff --git a/src/v2/index.ts b/src/v2/index.ts index 403673d..4082115 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -1,14 +1,13 @@ -import { Context, sort } from "./utils"; +import { Context } from "./utils"; import { Side } from "../shared/language"; import { Iterator } from "./iterator"; import { computeDiff } from "./core"; import { fail } from "../debug"; import { LanguageConfigs, Options, OutputType, ResultTypeMapper, Segment } from "./types"; import { applyChangesToSources } from "./printer"; -import { Change, Move } from "./change"; +import { Move } from "./change"; import { computeMoveAlignment } from "./semanticAligment"; -import { compactAndCreateDiff } from "./compact"; -import { DiffType } from "../types"; +import { getAdditionsAndDeletionsChanges } from "./compact"; export function getDiff2<_OutputType extends OutputType = OutputType.changes>( sourceA: string, @@ -34,20 +33,7 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>( moves = computeMoveAlignment(moves); } - // We compact all tracked additions and deletions into a single change with multiple segments, we also compact them if possible - - const changes: Change[] = moves; - if (_context.additions.length) { - // Sort before compacting - const additionsChange = compactAndCreateDiff(DiffType.addition, additions.sort((a, b) => sort.asc(a[1], b[1]))); - changes.push(additionsChange); - } - - if (_context.deletions.length) { - // Sort before compacting - const deletionsChange = compactAndCreateDiff(DiffType.deletion, deletions.sort((a, b) => sort.asc(a[0], b[0]))); - changes.push(deletionsChange); - } + const changes = [...moves, ...getAdditionsAndDeletionsChanges()]; switch (_options.outputType) { case OutputType.changes: { From 64776fe5ef4f07420d1a7135091ad4377c5f42a6 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 27 Mar 2024 12:26:20 -0300 Subject: [PATCH 51/79] Single node matching working stable now --- package.json | 4 ++-- src/v2/compilers.ts | 6 +++++- src/v2/core.ts | 15 ++++++++++++--- src/v2/pairedNodes.ts | 7 +++++++ src/v2/types.ts | 5 ++++- src/v2/utils.ts | 13 ++++++++++++- tests/conformance/changes.test.ts | 5 +++-- 7 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 src/v2/pairedNodes.ts diff --git a/package.json b/package.json index a232a03..939598e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "name": "Elian Cordoba" }, "scripts": { - "test": "vitest -c ./vitest.config.js", + "test": "vitest -c ./vitest.config.mjs", "testNew": "vitest -c ./vitest.config.mjs additions-removals changes move sequence-matching trivia-diff", "test-v2": "vitest -c ./vitest.config.mjs ./tests/v2", "build": "tsc --watch", @@ -39,4 +39,4 @@ "vite": "4.4.2", "vitest": "1.4.0" } -} \ No newline at end of file +} diff --git a/src/v2/compilers.ts b/src/v2/compilers.ts index ddc1499..4303ec9 100644 --- a/src/v2/compilers.ts +++ b/src/v2/compilers.ts @@ -3,6 +3,7 @@ import { Node } from "./node"; import { getSourceFile } from "../frontend/utils"; import { Side } from "../shared/language"; import { NodesTable, ParsedProgram } from "./types"; +import { _context } from "."; export function getTsNodes(source: string, side: Side): ParsedProgram { const ast = getSourceFile(source); @@ -89,5 +90,8 @@ export function canNodeBeMatchedAlone(node: TsNode) { const isTemplateString = kind === SyntaxKind.FirstTemplateToken || kind === SyntaxKind.LastTemplateToken; const other = kind === SyntaxKind.DebuggerKeyword; - return (isLiteral || isIdentifier || isTemplateString || other) && ((node as any).text || "").length >= 1; + const isCorrectKind = isLiteral || isIdentifier || isTemplateString || other; + const textIsLongEnough = ((node as any).text || "").length >= _context.options.minNumberOfCharactersInMoves; + + return isCorrectKind && textIsLongEnough; } diff --git a/src/v2/core.ts b/src/v2/core.ts index 3cf1d37..3a735cf 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -6,6 +6,7 @@ import { DiffType } from "../types"; import { Change } from "./change"; import { range } from "../utils"; import { Segment } from "./types"; +import { isPairedNode } from "./pairedNodes"; export function computeDiff() { const { iterA, iterB, moves, additions, deletions } = _context; @@ -44,7 +45,8 @@ export function computeDiff() { // 2.b const remainingSegments = []; for (const segment of bestMatchForB.segments) { - if (segment[2] === 1 && !checkIfNodeCanBeMatchedAlone(segment)) { + // Only one-length segments need to be checked + if (segment[2] === 1 && !canNodeCanBeMatchedAlone(segment)) { const nodeA = iterA.nodes[segment[0]]; const nodeB = iterB.nodes[segment[1]]; @@ -109,9 +111,16 @@ export function oneSidedIteration( } } -function checkIfNodeCanBeMatchedAlone(segment: Segment) { +// The single matching node functionality is present so that we don't match a random "let" or ";" or "=" with some other radom similar node. +// But, we don't just want to disable single-node matches altogether, for example, a "secret-key-123" string or a 8080 port number could be meaningfully matched. +function canNodeCanBeMatchedAlone(segment: Segment) { const nodeIndex = segment[0]; const node = _context.iterA.nodes[nodeIndex]; - return _context.languageConfig.singleNodeMatchingFn(node); + // We won't consider paired nodes ("(", ")", "{", "}", "[", "]") here, their matching is taken care of by the "pairedNodesVerifier" + if (isPairedNode(node.kind)) { + return true; + } + + return _context.languageConfig.canNodeBeMatchedAlone(node); } diff --git a/src/v2/pairedNodes.ts b/src/v2/pairedNodes.ts new file mode 100644 index 0000000..6b592cc --- /dev/null +++ b/src/v2/pairedNodes.ts @@ -0,0 +1,7 @@ +import { _context } from "."; + +export function isPairedNode(kind: number) { + const { languageConfig } = _context; + + return languageConfig.pairedNodes.some((pair) => pair[0] === kind || pair[1] === kind); +} diff --git a/src/v2/types.ts b/src/v2/types.ts index 5756ffa..ea50186 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -13,13 +13,16 @@ export interface ParsedProgram { export interface LanguageConfigs { language: string; - singleNodeMatchingFn: (node: any) => boolean; + canNodeBeMatchedAlone: (node: any) => boolean; + pairedNodes: [SyntaxKind, SyntaxKind][]; } export interface Options<_OutputType extends OutputType = OutputType.changes> { outputType?: _OutputType; tryAlignMoves?: boolean; maxNodeSkips?: number; + // Matches with less than this number of characters in it (whitespace included) will be ignored. This is a measure to reduce noise + minNumberOfCharactersInMoves?: number; } export enum OutputType { diff --git a/src/v2/utils.ts b/src/v2/utils.ts index f065a39..1a4b263 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -9,15 +9,26 @@ import { LanguageConfigs, Options, OutputType, Segment } from "./types"; import { CandidateMatch } from "./match"; import { canNodeBeMatchedAlone } from "./compilers"; +import { SyntaxKind } from "typescript"; + const defaultLanguageConfigs: LanguageConfigs = { language: "typescript", - singleNodeMatchingFn: canNodeBeMatchedAlone, + canNodeBeMatchedAlone, + pairedNodes: [ + // { } + [SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken], + // ( ) + [SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken], + // [ ] + [SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken], + ], }; const defaultOptions: Required = { outputType: OutputType.changes, tryAlignMoves: true, maxNodeSkips: 5, + minNumberOfCharactersInMoves: 1, }; export class Context<_OutputType extends OutputType = OutputType.changes> { diff --git a/tests/conformance/changes.test.ts b/tests/conformance/changes.test.ts index ac03aed..8ba9a3a 100644 --- a/tests/conformance/changes.test.ts +++ b/tests/conformance/changes.test.ts @@ -43,13 +43,14 @@ describe("Properly report line changed", () => { let firstName = "elian" `, expA: ` - let ➖name➖ = "elian" + ➖let name➖ = "elian" `, expB: ` - let ➕firstName➕ = "elian" + ➕let firstName➕ = "elian" `, }); + // TODO(Future): Once we implement changes, we will be able to narrow down this diff test({ name: "Single line change 4", a: ` From 20a2fd96a99a59a75905db0d0de621c73f5e53dd Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 27 Mar 2024 14:17:45 -0300 Subject: [PATCH 52/79] Rename --- src/v2/printer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/v2/printer.ts b/src/v2/printer.ts index 4b00f58..bb994dd 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -35,7 +35,7 @@ export function applyChangesToSources( endIndex - 1, ); - charsB = getSourceWithChange( + charsB = highlightRangeInSource( charsB, start, end, @@ -54,7 +54,7 @@ export function applyChangesToSources( endIndex - 1, ); - charsA = getSourceWithChange( + charsA = highlightRangeInSource( charsA, start, end, @@ -68,7 +68,7 @@ export function applyChangesToSources( return { sourceA: charsA.join(""), sourceB: charsB.join("") }; } -export function getSourceWithChange( +export function highlightRangeInSource( chars: string[], start: number, end: number, @@ -195,7 +195,7 @@ export function getPrettyStringFromChange( a.start, a.end - 1, ); - charsA = getSourceWithChange(charsA, startA, endA, color, true); + charsA = highlightRangeInSource(charsA, startA, endA, color, true); } if (change.type & TypeMasks.AddOrMove) { @@ -204,7 +204,7 @@ export function getPrettyStringFromChange( b.start, b.end - 1, ); - charsB = getSourceWithChange(charsB, startB, endB, color, true); + charsB = highlightRangeInSource(charsB, startB, endB, color, true); } } From 81732b9294e06566b4ed9e5996709b9e47bbc1d4 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 27 Mar 2024 15:29:13 -0300 Subject: [PATCH 53/79] Refactored printing and debug functions --- src/v2/debug.ts | 81 ++++++++++++++++ src/v2/index.ts | 6 +- src/v2/iterator.ts | 5 +- src/v2/printer.ts | 225 +++++++++++++-------------------------------- 4 files changed, 152 insertions(+), 165 deletions(-) create mode 100644 src/v2/debug.ts diff --git a/src/v2/debug.ts b/src/v2/debug.ts new file mode 100644 index 0000000..5478804 --- /dev/null +++ b/src/v2/debug.ts @@ -0,0 +1,81 @@ +import Table from "cli-table3"; +import colorFn from "kleur"; + +import { createTextTable, prettyRenderFn } from "../debug"; +import { Change } from "./change"; +import { highlightChanges } from "./printer"; +import { capitalizeFirstLetter } from "./utils"; +import { DiffType } from "../types"; +import { rangeEq } from "../utils"; + +export function prettyPrintSources(a: string, b: string) { + console.log(createTextTable(a, b)); +} + +export function prettyPrintChanges(a: string, b: string, changes: Change[]) { + const sourcesWithChanges = highlightChanges( + a, + b, + changes, + true, + ); + console.log( + createTextTable(sourcesWithChanges.sourceA, sourcesWithChanges.sourceB), + ); +} + +export function prettyPrintChangesInSequence( + sourceA: string, + sourceB: string, + changes: Change[], + options: { sortByLength: boolean } = { sortByLength: true }, +) { + const table = new Table({ + head: [ + colorFn.magenta("Type"), + colorFn.blue("Length"), + colorFn.grey("Skips"), + colorFn.cyan("Start"), + colorFn.yellow("Line Nº"), + colorFn.red("Source"), + colorFn.green("Revision"), + ], + colAligns: ["center", "center", "center", "center"], + }); + + const sortedByLength = options.sortByLength ? changes.sort((a, b) => (a.textLength < b.textLength ? 1 : -1)) : changes; + + const lineNumberString = getLinesOfCodeString(sourceA, sourceB); + + for (const change of sortedByLength) { + const changeName = capitalizeFirstLetter(DiffType[change.type]); + const changeColorFn = prettyRenderFn[change.type]; + + const sourceWithChange = highlightChanges(sourceA, sourceB, [change], true); + + table.push([ + changeColorFn(changeName), + colorFn.blue(change.textLength), + colorFn.grey(change.skips || "-"), + colorFn.cyan(`"${change.startNode.text}"`), + colorFn.yellow(lineNumberString), + sourceWithChange.sourceA, + sourceWithChange.sourceB, + ]); + } + + console.log(table.toString()); +} + +function getLinesOfCodeString(a: string, b: string) { + const aLines = a.split("\n"); + const bLines = b.split("\n"); + const linesOfCode = Math.max(aLines.length, bLines.length); + + let str = ""; + for (const lineNumber of rangeEq(1, linesOfCode)) { + str += `${lineNumber}\n`; + } + + return str; +} diff --git a/src/v2/index.ts b/src/v2/index.ts index 4082115..296c4f0 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -4,7 +4,7 @@ import { Iterator } from "./iterator"; import { computeDiff } from "./core"; import { fail } from "../debug"; import { LanguageConfigs, Options, OutputType, ResultTypeMapper, Segment } from "./types"; -import { applyChangesToSources } from "./printer"; +import { highlightChanges } from "./printer"; import { Move } from "./change"; import { computeMoveAlignment } from "./semanticAligment"; import { getAdditionsAndDeletionsChanges } from "./compact"; @@ -40,10 +40,10 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>( return changes as ResultTypeMapper[_OutputType]; } case OutputType.text: { - return applyChangesToSources(sourceA, sourceB, changes, false) as ResultTypeMapper[_OutputType]; + return highlightChanges(sourceA, sourceB, changes, false) as ResultTypeMapper[_OutputType]; } case OutputType.prettyText: { - return applyChangesToSources(sourceA, sourceB, changes, true) as ResultTypeMapper[_OutputType]; + return highlightChanges(sourceA, sourceB, changes, true) as ResultTypeMapper[_OutputType]; } default: fail("Unknown output type"); diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index 9a6b2e9..ead19ae 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -8,6 +8,7 @@ import { getTsNodes } from "./compilers"; import { Node } from "./node"; import { NodesTable } from "./types"; import { DiffType } from "../types"; +import { highlightRange } from "./printer"; export class Iterator { nodes: Node[]; @@ -84,12 +85,12 @@ export class Iterator { const { start, end } = node.getRange(); const color = node.isTextNode ? colorFn.magenta : colorFn.yellow; - const result = getSourceWithChange(chars, start, end, color); + const result = highlightRange(chars, start, end, color, true); console.log(result.join("")); } - printNodes() { + printAllNodes() { const table = new Table({ head: [ colorFn.blue("Index"), diff --git a/src/v2/printer.ts b/src/v2/printer.ts index bb994dd..474a02a 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -1,15 +1,14 @@ -import Table from "cli-table3"; import colorFn from "kleur"; -import { asciiRenderFn, assert, createTextTable, fail, prettyRenderFn, RenderFn } from "../debug"; +import { asciiRenderFn, assert, prettyRenderFn, RenderFn } from "../debug"; import { DiffType, TypeMasks } from "../types"; -import { capitalizeFirstLetter, getIndexesFromSegment } from "./utils"; +import { getIndexesFromSegment } from "./utils"; import { Iterator } from "./iterator"; import { _context } from "."; -import { rangeEq } from "../utils"; import { Change } from "./change"; +import { Segment } from "./types"; -export function applyChangesToSources( +export function highlightChanges( sourceA: string, sourceB: string, changes: Change[], @@ -19,56 +18,74 @@ export function applyChangesToSources( let charsA = sourceA.split(""); let charsB = sourceB.split(""); - const { iterA, iterB } = _context; - for (const change of changes) { - const renderWith = renderFn[change.type]; for (const segment of change.segments) { - if (TypeMasks.AddOrMove & change.type) { - const { b } = getIndexesFromSegment(segment); - - const { start: startIndex, end: endIndex } = b; - - const [start, end] = getStringPositionsFromRange( - iterB, - startIndex, - endIndex - 1, - ); - - charsB = highlightRangeInSource( - charsB, - start, - end, - renderWith, - forDebug, - ); - } - - if (TypeMasks.DelOrMove & change.type) { - const { a } = getIndexesFromSegment(segment); - - const { start: startIndex, end: endIndex } = a; - const [start, end] = getStringPositionsFromRange( - iterA, - startIndex, - endIndex - 1, - ); - - charsA = highlightRangeInSource( - charsA, - start, - end, - renderWith, - forDebug, - ); - } + const res = highlightSegment(charsA, charsB, change.type, segment, renderFn[change.type], forDebug); + charsA = res.charsA; + charsB = res.charsB; } } return { sourceA: charsA.join(""), sourceB: charsB.join("") }; } -export function highlightRangeInSource( +// Apply the highlighting to both sources based on a given segment +function highlightSegment( + charsA: string[], + charsB: string[], + sideToRender: DiffType, + segment: Segment, + renderWith: RenderFn, + forDebug: boolean, +) { + const { iterA, iterB } = _context; + + if (TypeMasks.AddOrMove & sideToRender) { + const { b } = getIndexesFromSegment(segment); + + const { start: startIndex, end: endIndex } = b; + + const [start, end] = getStringPositionsFromRange( + iterB, + startIndex, + endIndex - 1, + ); + + charsB = highlightRange( + charsB, + start, + end, + renderWith, + forDebug, + ); + } + + if (TypeMasks.DelOrMove & sideToRender) { + const { a } = getIndexesFromSegment(segment); + + const { start: startIndex, end: endIndex } = a; + const [start, end] = getStringPositionsFromRange( + iterA, + startIndex, + endIndex - 1, + ); + + charsA = highlightRange( + charsA, + start, + end, + renderWith, + forDebug, + ); + } + + return { + charsA, + charsB, + }; +} + +export function highlightRange( chars: string[], start: number, end: number, @@ -126,7 +143,7 @@ export function highlightRangeInSource( return [...head, text, ...compliment, ...tail]; } -export function getComplimentArray( +function getComplimentArray( length: number, fillInCharacter = "", ): string[] { @@ -173,115 +190,3 @@ export function getColor() { function resetColorRoulette() { _currentColor = 0; } - -export function getPrettyStringFromChange( - change: Change, - sourceA: string, - sourceB: string, -) { - const { iterA, iterB } = _context; - let charsA = sourceA.split(""); - let charsB = sourceB.split(""); - - for (const segment of change.segments) { - // switch diff type render add del move separtely - const { a, b } = getIndexesFromSegment(segment); - - const color = prettyRenderFn[change.type]; - - if (change.type & TypeMasks.DelOrMove) { - const [startA, endA] = getStringPositionsFromRange( - iterA, - a.start, - a.end - 1, - ); - charsA = highlightRangeInSource(charsA, startA, endA, color, true); - } - - if (change.type & TypeMasks.AddOrMove) { - const [startB, endB] = getStringPositionsFromRange( - iterB, - b.start, - b.end - 1, - ); - charsB = highlightRangeInSource(charsB, startB, endB, color, true); - } - } - - return { - a: charsA.join(""), - b: charsB.join(""), - }; -} - -export function prettyPrintSources(a: string, b: string) { - console.log(createTextTable(a, b)); -} - -export function prettyPrintChanges(a: string, b: string, changes: Change[]) { - const sourcesWithChanges = applyChangesToSources( - a, - b, - changes, - true, - ); - console.log( - createTextTable(sourcesWithChanges.sourceA, sourcesWithChanges.sourceB), - ); -} - -export function prettyPrintChangesInSequence( - a: string, - b: string, - changes: Change[], - options: { sortByLength: boolean } = { sortByLength: true }, -) { - const table = new Table({ - head: [ - colorFn.magenta("Type"), - colorFn.blue("Length"), - colorFn.grey("Skips"), - colorFn.cyan("Start"), - colorFn.yellow("Line Nº"), - colorFn.red("Source"), - colorFn.green("Revision"), - ], - colAligns: ["center", "center", "center", "center"], - }); - - const sortedByLength = options.sortByLength ? changes.sort((a, b) => (a.textLength < b.textLength ? 1 : -1)) : changes; - - const lineNumberString = getLinesOfCodeString(a, b); - - for (const change of sortedByLength) { - const changeName = capitalizeFirstLetter(DiffType[change.type]); - const changeColorFn = prettyRenderFn[change.type]; - - const sourceWithChange = getPrettyStringFromChange(change, a, b); - - table.push([ - changeColorFn(changeName), - colorFn.blue(change.textLength), - colorFn.grey(change.skips || "-"), - colorFn.cyan(`"${change.startNode.text}"`), - colorFn.yellow(lineNumberString), - sourceWithChange.a, - sourceWithChange.b, - ]); - } - - console.log(table.toString()); -} - -function getLinesOfCodeString(a: string, b: string) { - const aLines = a.split("\n"); - const bLines = b.split("\n"); - const linesOfCode = Math.max(aLines.length, bLines.length); - - let str = ""; - for (const lineNumber of rangeEq(1, linesOfCode)) { - str += `${lineNumber}\n`; - } - - return str; -} From df36c4ba1ba728cb83ee33ffbf51eb812bc4e6a5 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 27 Mar 2024 16:22:09 -0300 Subject: [PATCH 54/79] move code to fn --- src/v2/core.ts | 64 +++++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/v2/core.ts b/src/v2/core.ts index 3a735cf..0585991 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -7,6 +7,7 @@ import { Change } from "./change"; import { range } from "../utils"; import { Segment } from "./types"; import { isPairedNode } from "./pairedNodes"; +import { CandidateMatch } from "./match"; export function computeDiff() { const { iterA, iterB, moves, additions, deletions } = _context; @@ -42,33 +43,14 @@ export function computeDiff() { // 2- Get all possible matches from subsequence nodes and compare it with the original match found. Pick the best bestMatchForB = getBestMatchFromSubsequenceNodes(bestMatchForB, b); - // 2.b - const remainingSegments = []; - for (const segment of bestMatchForB.segments) { - // Only one-length segments need to be checked - if (segment[2] === 1 && !canNodeCanBeMatchedAlone(segment)) { - const nodeA = iterA.nodes[segment[0]]; - const nodeB = iterB.nodes[segment[1]]; + // 3 + bestMatchForB = filterOutSingleSingleNodeFromMatch(bestMatchForB); - deletions.push(nodeA.getSegment(DiffType.deletion)); - additions.push(nodeB.getSegment(DiffType.addition)); - - iterA.mark(nodeA.index, DiffType.deletion); - iterB.mark(nodeB.index, DiffType.addition); - } else { - remainingSegments.push(segment); - } - } - - if (remainingSegments.length === 0) { - continue; - } else if (remainingSegments.length !== bestMatchForB.segments.length) { - // TODO-2: Update skips as well - bestMatchForB.segments = remainingSegments; - bestMatchForB.textLength = getTextLengthFromSegments(remainingSegments); + if (!bestMatchForB) { + continue } - // 3- Store the match, mark nodes and continue + // 4- Store the match, mark nodes and continue const move = Change.createMove(bestMatchForB); moves.push(move); markMatched(move); @@ -124,3 +106,37 @@ function canNodeCanBeMatchedAlone(segment: Segment) { return _context.languageConfig.canNodeBeMatchedAlone(node); } + +function getBestMatchCheckingBackwards(match: CandidateMatch) { + +} + +function filterOutSingleSingleNodeFromMatch(match: CandidateMatch) { + const { iterA, iterB, additions, deletions } = _context + const remainingSegments = []; + for (const segment of match.segments) { + // Only one-length segments need to be checked + if (segment[2] === 1 && !canNodeCanBeMatchedAlone(segment)) { + const nodeA = iterA.nodes[segment[0]]; + const nodeB = iterB.nodes[segment[1]]; + + deletions.push(nodeA.getSegment(DiffType.deletion)); + additions.push(nodeB.getSegment(DiffType.addition)); + + iterA.mark(nodeA.index, DiffType.deletion); + iterB.mark(nodeB.index, DiffType.addition); + } else { + remainingSegments.push(segment); + } + } + + if (remainingSegments.length === 0) { + return + } else if (remainingSegments.length !== match.segments.length) { + // TODO-2: Update skips as well + match.segments = remainingSegments; + match.textLength = getTextLengthFromSegments(remainingSegments); + } + + return match +} \ No newline at end of file From fe6908064b6f7cc0b44ccbc48ae59d01e431e38a Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 3 Apr 2024 17:17:47 -0300 Subject: [PATCH 55/79] Added basic tracing --- src/v2/core.ts | 70 ++++++++++++++++++++++++-------------- src/v2/debug.ts | 22 +++++++++++- src/v2/diff.ts | 21 ++++++++++++ src/v2/index.ts | 5 +++ src/v2/iterator.ts | 4 +-- src/v2/node.ts | 2 +- src/v2/printer.ts | 2 +- src/v2/tracing.ts | 84 ++++++++++++++++++++++++++++++++++++++++++++++ src/v2/types.ts | 1 + src/v2/utils.ts | 14 ++++++++ 10 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 src/v2/tracing.ts diff --git a/src/v2/core.ts b/src/v2/core.ts index 0585991..2a1f66c 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -1,3 +1,4 @@ +import colorFn from "kleur"; import { _context } from "."; import { getBestMatch, getBestMatchFromSubsequenceNodes } from "./diff"; import { getIndexesFromSegment, getTextLengthFromSegments, matchContainsSingleNode, sort } from "./utils"; @@ -8,6 +9,7 @@ import { range } from "../utils"; import { Segment } from "./types"; import { isPairedNode } from "./pairedNodes"; import { CandidateMatch } from "./match"; +import { trace } from "./tracing"; export function computeDiff() { const { iterA, iterB, moves, additions, deletions } = _context; @@ -17,6 +19,7 @@ export function computeDiff() { const b = iterB.next(); if (!a && !b) { + trace(colorFn.magenta("Exit: Both iter finished")); break; } @@ -26,30 +29,41 @@ export function computeDiff() { const iterOn = !a ? iterB : iterA; const type = !a ? DiffType.addition : DiffType.deletion; + trace(colorFn.magenta(`Exit: Iter ${iterOn.side.toUpperCase()} finished`)); + oneSidedIteration(iterOn, type); break; } + trace(colorFn.cyan("About to process nodes"), { type: "nodes", a, b }); + // 1- Get the best match for the current node let bestMatchForB = getBestMatch(b); if (!bestMatchForB) { + trace({ type: "change", diffType: DiffType.addition }, "Node B wasn't found on A", b); + additions.push(b.getSegment(DiffType.addition)); iterB.mark(b.index, DiffType.addition); continue; } + trace("Best match for B", b, bestMatchForB); + // 2- Get all possible matches from subsequence nodes and compare it with the original match found. Pick the best bestMatchForB = getBestMatchFromSubsequenceNodes(bestMatchForB, b); + // bestMatchForB = getBestMatchCheckingBackwards(bestMatchForB, b); + // 3 bestMatchForB = filterOutSingleSingleNodeFromMatch(bestMatchForB); if (!bestMatchForB) { - continue + continue; } + trace("Marking as move and continuing"); // 4- Store the match, mark nodes and continue const move = Change.createMove(bestMatchForB); moves.push(move); @@ -108,35 +122,39 @@ function canNodeCanBeMatchedAlone(segment: Segment) { } function getBestMatchCheckingBackwards(match: CandidateMatch) { - } function filterOutSingleSingleNodeFromMatch(match: CandidateMatch) { - const { iterA, iterB, additions, deletions } = _context + const { iterA, iterB, additions, deletions } = _context; const remainingSegments = []; - for (const segment of match.segments) { - // Only one-length segments need to be checked - if (segment[2] === 1 && !canNodeCanBeMatchedAlone(segment)) { - const nodeA = iterA.nodes[segment[0]]; - const nodeB = iterB.nodes[segment[1]]; - - deletions.push(nodeA.getSegment(DiffType.deletion)); - additions.push(nodeB.getSegment(DiffType.addition)); - - iterA.mark(nodeA.index, DiffType.deletion); - iterB.mark(nodeB.index, DiffType.addition); - } else { - remainingSegments.push(segment); - } - } + for (const segment of match.segments) { + // Only one-length segments need to be checked + if (segment[2] === 1 && !canNodeCanBeMatchedAlone(segment)) { + trace("Will filter out this segment from the match is it's a single node segment", { type: "segment", segment }); - if (remainingSegments.length === 0) { - return - } else if (remainingSegments.length !== match.segments.length) { - // TODO-2: Update skips as well - match.segments = remainingSegments; - match.textLength = getTextLengthFromSegments(remainingSegments); + const nodeA = iterA.nodes[segment[0]]; + const nodeB = iterB.nodes[segment[1]]; + + deletions.push(nodeA.getSegment(DiffType.deletion)); + additions.push(nodeB.getSegment(DiffType.addition)); + + iterA.mark(nodeA.index, DiffType.deletion); + iterB.mark(nodeB.index, DiffType.addition); + } else { + remainingSegments.push(segment); } + } - return match -} \ No newline at end of file + if (remainingSegments.length === 0) { + trace("All segments were filtered out from match"); + return; + } else if (remainingSegments.length !== match.segments.length) { + trace(`Match went from ${match.segments.length} segments to ${remainingSegments.length}`); + + // TODO-2: Update skips as well + match.segments = remainingSegments; + match.textLength = getTextLengthFromSegments(remainingSegments); + } + + return match; +} diff --git a/src/v2/debug.ts b/src/v2/debug.ts index 5478804..671caff 100644 --- a/src/v2/debug.ts +++ b/src/v2/debug.ts @@ -3,10 +3,12 @@ import colorFn from "kleur"; import { createTextTable, prettyRenderFn } from "../debug"; import { Change } from "./change"; -import { highlightChanges } from "./printer"; +import { highlightChanges, highlightSegment } from "./printer"; import { capitalizeFirstLetter } from "./utils"; import { DiffType } from "../types"; import { rangeEq } from "../utils"; +import { Segment } from "./types"; +import { _context } from "."; export function prettyPrintSources(a: string, b: string) { console.log(createTextTable(a, b)); @@ -24,6 +26,24 @@ export function prettyPrintChanges(a: string, b: string, changes: Change[]) { ); } +export function prettyPrintSegment(segment: Segment, type: DiffType = DiffType.move) { + const res = highlightSegment( + _context.sourceA.split(""), + _context.sourceB.split(""), + type, + segment, + prettyRenderFn[type], + true, + ); + + const sourceA = res.charsA.join(""); + const sourceB = res.charsB.join(""); + + const str = createTextTable(sourceA, sourceB); + + return str; +} + export function prettyPrintChangesInSequence( sourceA: string, sourceB: string, diff --git a/src/v2/diff.ts b/src/v2/diff.ts index b5b1a16..942816c 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -1,3 +1,4 @@ +import colorFn from "kleur"; import { _context } from "."; import { assert, fail } from "../debug"; import { Iterator } from "./iterator"; @@ -8,11 +9,17 @@ import { Segment } from "./types"; import { CandidateMatch } from "./match"; import { Side } from "../shared/language"; import { DiffType } from "../types"; +import { trace } from "./tracing"; /** * Returns the longest possible match for the given node, this is including possible skips to improve the match length. */ export function getBestMatch(node: Node): CandidateMatch | undefined { + const alreadyMatched = _context.verifyIfNodeWasMatched(node); + if (alreadyMatched) { + trace(colorFn.red("Already matched"), node); + } + // Since this function can be called with both A-sided and B-sided nodes, we need to get the iter dynamically // The iterator we need is the opposite of the given node since there is where we need to get the match from const sideToLook = oppositeSide(node.side); @@ -34,6 +41,7 @@ export function getBestMatch(node: Node): CandidateMatch | undefined { } } + _context.storeMatch(node, bestMatch); return bestMatch; } @@ -237,12 +245,17 @@ export function getBestMatchFromSubsequenceNodes(currentBestMatch: CandidateMatc // May be empty if the node we are looking for was the only one const subSequenceNodesToCheck = getSubSequenceNodes(currentBestMatch, b); + trace("Checking subsequence nodes", subSequenceNodesToCheck.length === 0 ? "None" : subSequenceNodesToCheck.map((n) => `Index: ${n.index} "${n.text}"`)); + let bestSubsequenceMatch = currentBestMatch; + let foundBetterMatch = false; for (const node of subSequenceNodesToCheck) { const newCandidate = getBestMatch(node); if (!newCandidate) { + trace({ type: "change", diffType: DiffType.addition }, "Node B wasn't found on A", b); + additions.push(b.getSegment(DiffType.addition)); iterB.mark(b.index, DiffType.addition); @@ -250,9 +263,17 @@ export function getBestMatchFromSubsequenceNodes(currentBestMatch: CandidateMatc } if (newCandidate.isBetterThan(bestSubsequenceMatch)) { + trace("Found a better match", newCandidate); + foundBetterMatch = true; bestSubsequenceMatch = newCandidate; } } + if (foundBetterMatch) { + trace("Found a better match in the subsequence check"); + } else { + trace("No Better match in the subsequence nodes was found"); + } + return bestSubsequenceMatch; } diff --git a/src/v2/index.ts b/src/v2/index.ts index 296c4f0..8fe63d2 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -8,6 +8,7 @@ import { highlightChanges } from "./printer"; import { Move } from "./change"; import { computeMoveAlignment } from "./semanticAligment"; import { getAdditionsAndDeletionsChanges } from "./compact"; +import { trace } from "./tracing"; export function getDiff2<_OutputType extends OutputType = OutputType.changes>( sourceA: string, @@ -30,7 +31,11 @@ export function getDiff2<_OutputType extends OutputType = OutputType.changes>( moves = computeDiff(); if (_options.tryAlignMoves) { + trace(`Will align ${moves.length} moves`); moves = computeMoveAlignment(moves); + trace(`There are ${moves.length} moves after aligning`); + } else { + trace("Skipping move alignment"); } const changes = [...moves, ...getAdditionsAndDeletionsChanges()]; diff --git a/src/v2/iterator.ts b/src/v2/iterator.ts index ead19ae..e9ca3de 100644 --- a/src/v2/iterator.ts +++ b/src/v2/iterator.ts @@ -85,9 +85,9 @@ export class Iterator { const { start, end } = node.getRange(); const color = node.isTextNode ? colorFn.magenta : colorFn.yellow; - const result = highlightRange(chars, start, end, color, true); + const result = highlightRange(chars, start, end, color, true).join(""); - console.log(result.join("")); + return result; } printAllNodes() { diff --git a/src/v2/node.ts b/src/v2/node.ts index 38abfe0..21b2d59 100644 --- a/src/v2/node.ts +++ b/src/v2/node.ts @@ -98,6 +98,6 @@ export class Node { draw() { const iter = _context[this.side === Side.a ? "iterA" : "iterB"]; - iter.printNode(this); + return iter.printNode(this); } } diff --git a/src/v2/printer.ts b/src/v2/printer.ts index 474a02a..b72403c 100644 --- a/src/v2/printer.ts +++ b/src/v2/printer.ts @@ -30,7 +30,7 @@ export function highlightChanges( } // Apply the highlighting to both sources based on a given segment -function highlightSegment( +export function highlightSegment( charsA: string[], charsB: string[], sideToRender: DiffType, diff --git a/src/v2/tracing.ts b/src/v2/tracing.ts new file mode 100644 index 0000000..efc6fba --- /dev/null +++ b/src/v2/tracing.ts @@ -0,0 +1,84 @@ +import colorFn from "kleur"; +import { _context } from "."; +import { createTextTable, prettyRenderFn } from "../debug"; +import { DiffType } from "../types"; +import { Change } from "./change"; +import { prettyPrintChangesInSequence, prettyPrintSegment, prettyPrintSources } from "./debug"; +import { CandidateMatch } from "./match"; +import { Node } from "./node"; +import { Segment } from "./types"; + +type Traceable = TraceableNodePair | TraceableSegment | TraceableChange; + +interface TraceableNodePair { + type: "nodes"; + a: Node | undefined; + b: Node | undefined; +} + +interface TraceableSegment { + type: "segment"; + segment: Segment; + diffType?: DiffType; +} + +interface TraceableChange { + type: "change"; + diffType: DiffType.addition | DiffType.deletion; +} + +type Primitive = string | number | boolean; + +let traceNumber = 0; +export function trace(...args: Array | Node | CandidateMatch | Traceable>) { + if (!_context.options.enableTracingLogs) { + return; + } + + traceNumber++; + + for (const arg of args) { + let str = ""; + if (isPrimitive(arg)) { + str = String("\n" + colorFn.yellow("[TRACING]: ") + arg); + } else if (Array.isArray(arg)) { + str = arg.join(", "); + } else if (arg instanceof Node) { + str = arg.draw(); + } else if (arg instanceof CandidateMatch) { + const move = Change.createMove(arg); + + prettyPrintChangesInSequence( + _context.sourceA, + _context.sourceB, + [move], + ); + } else if (arg.type === "nodes") { + const { a, b } = arg as TraceableNodePair; + + const sourceA = !a ? "---" : a.draw(); + const sourceB = !b ? "---" : b.draw(); + + str = createTextTable(sourceA, sourceB); + } else if (arg.type === "segment") { + str = prettyPrintSegment(arg.segment, arg.diffType); + } else if (arg.type === "change") { + if (arg.diffType === DiffType.addition) { + str = prettyRenderFn[DiffType.addition]("Addition: "); + } else { + str = prettyRenderFn[DiffType.addition]("Deletion: "); + } + } else { + console.log("Unknown type", arg); + str = arg; + } + + console.log(str); + } + + console.log("\n" + traceNumber, "───────────────────────────────────"); +} + +function isPrimitive(value: any): value is Primitive { + return value !== Object(value); +} diff --git a/src/v2/types.ts b/src/v2/types.ts index ea50186..0092697 100644 --- a/src/v2/types.ts +++ b/src/v2/types.ts @@ -23,6 +23,7 @@ export interface Options<_OutputType extends OutputType = OutputType.changes> { maxNodeSkips?: number; // Matches with less than this number of characters in it (whitespace included) will be ignored. This is a measure to reduce noise minNumberOfCharactersInMoves?: number; + enableTracingLogs?: boolean; } export enum OutputType { diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 1a4b263..76f59f4 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -29,6 +29,7 @@ const defaultOptions: Required = { tryAlignMoves: true, maxNodeSkips: 5, minNumberOfCharactersInMoves: 1, + enableTracingLogs: false, }; export class Context<_OutputType extends OutputType = OutputType.changes> { @@ -36,6 +37,11 @@ export class Context<_OutputType extends OutputType = OutputType.changes> { languageConfig: LanguageConfigs; options: Required>; + // number = b side index + computedMatches = { + a: new Map(), + b: new Map(), + }; constructor( languageConfig: Partial | undefined, @@ -51,6 +57,14 @@ export class Context<_OutputType extends OutputType = OutputType.changes> { this.languageConfig = { ...defaultLanguageConfigs, ...(languageConfig || {}) }; this.options = { ...defaultOptions, ...(options || {}) }; } + + verifyIfNodeWasMatched(node: Node) { + return this.computedMatches[node.side].get(node.index); + } + + storeMatch(node: Node, match: CandidateMatch) { + this.computedMatches[node.side].set(node.index, match); + } } export function equals(nodeOne: Node, nodeTwo: Node): boolean { From a916509a7b2d2e8935fe6d299ba50f788317b2bf Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 5 Apr 2024 17:21:52 -0300 Subject: [PATCH 56/79] Rename --- src/v2/diff.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 942816c..3f34b6e 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -34,7 +34,7 @@ export function getBestMatch(node: Node): CandidateMatch | undefined { let bestMatch = CandidateMatch.createEmpty(); for (const candidate of otherSideCandidates) { - const newCandidate = node.side === Side.a ? getCandidateMatch(node, candidate) : getCandidateMatch(candidate, node); + const newCandidate = node.side === Side.a ? exploreMatch(node, candidate) : exploreMatch(candidate, node); if (newCandidate.isBetterThan(bestMatch)) { bestMatch = newCandidate; @@ -63,7 +63,7 @@ function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { return [...allNodes]; } -function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { +function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); const MAX_NODE_SKIPS = _context.options.maxNodeSkips; @@ -178,7 +178,7 @@ function getCandidateMatch(nodeA: Node, nodeB: Node): CandidateMatch { let bestCandidate = CandidateMatch.createEmpty(); - // TODO instead of getBestMatch use getCandidateMatch so that it skips node if necessary + // TODO instead of getBestMatch use exploreMatch so that it skips node if necessary // TODO also getBestMatch has hardcoded B side for (const node of sameNodesAheadA) { From 58ec22c2a7b50caa09c0d525327adda063415a92 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 5 Apr 2024 18:22:19 -0300 Subject: [PATCH 57/79] Cleanups and comments --- src/debug.ts | 2 +- src/v2/diff.ts | 37 +++++++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/debug.ts b/src/debug.ts index a15a277..74432cf 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -91,7 +91,7 @@ export function createTextTable( colorFn.green("Revision"), ], colAligns: ["left", "left"], - colWidths: [5, 30, 30], + colWidths: [5, 50, 50], style: { compact: true, }, diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 3f34b6e..6cdc98f 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -63,10 +63,14 @@ function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { return [...allNodes]; } -function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { - assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); +export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { + const forwardPass = exploreMatchForward(nodeA, nodeB); + + return forwardPass +} - const MAX_NODE_SKIPS = _context.options.maxNodeSkips; +function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { + assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); const segments: Segment[] = []; const { iterA, iterB } = _context; @@ -119,8 +123,8 @@ function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { // A B C B C B B // 0 1 2 3 4 5 6 // - // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6 - function getSameNodesAhead( + // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped + function scanForSameNodes( iter: Iterator, wantedNode: Node, lastIndex: number, @@ -140,8 +144,21 @@ function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { return nodes; } - const skipBUntil = Math.min(iterB.nodes.length, indexB + MAX_NODE_SKIPS); - + // We will skip N nodes on A before skipping 1 node on B, up to N skips in B. N = maxNodeSkips defined in the options + // For example: + // + // A: 1 2 w x y z 4 5 + // B: 1 2 3 4 5 + // + // - We match "1 2", match breaks when we react "3" on B + // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips + // - We skip on B, now it's "4", we start again from the original A position, which is "w" + // - After 4 skips we found a match + // - Final results it's: + // 1 2 _ _ _ _ 4 5 + // 1 2 _ 4 5 + + const skipBUntil = Math.min(iterB.nodes.length, indexB + _context.options.maxNodeSkips); for (const newIndexB of range(indexB, skipBUntil)) { const newB = iterB.peek(newIndexB); @@ -151,7 +168,7 @@ function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { let skipsInLookaheadA = 0; - const skipAUntil = Math.min(iterA.nodes.length, indexA + MAX_NODE_SKIPS); + const skipAUntil = Math.min(iterA.nodes.length, indexA + _context.options.maxNodeSkips); lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { assert(newB); @@ -162,8 +179,8 @@ function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { } if (equals(newA, newB)) { - const sameNodesAheadA = getSameNodesAhead(iterA, newA, skipAUntil); - const sameNodesAheadB = getSameNodesAhead(iterB, newB, skipBUntil); + const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); + const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); if (!sameNodesAheadA.length && !sameNodesAheadB.length) { indexA = newIndexA; From 227913701033af3ccfa4fd4c9585badc46267aaa Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 8 Apr 2024 12:07:42 -0300 Subject: [PATCH 58/79] Updated comments --- src/debug.ts | 2 ++ src/v2/diff.ts | 25 ++++++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/debug.ts b/src/debug.ts index 74432cf..2e3fdf9 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -3,6 +3,7 @@ import Table from "cli-table3"; import { DiffType } from "./types"; import colorFn from "kleur"; import { _context as _context2 } from "./v2/index"; +import { trace } from "./v2/tracing"; enum ErrorType { DebugFailure = "DebugFailure", @@ -35,6 +36,7 @@ class DebugFailure extends BaseError { } export function fail(errorMessage?: string): never { + trace(colorFn.red("Assertion failed"), errorMessage || ""); throw new DebugFailure(errorMessage || "Assertion failed"); } diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 6cdc98f..a4ab434 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -178,11 +178,23 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { break lookaheadA; } + // After the appropriate skips we need to test if we found a match if (equals(newA, newB)) { + // Because we may had skipped on A and B we need to ensure that the nodes from the new match are the best candidate + // Since there may be same nodes ahead, for example, given: + // + // A: 1 2 3 4 + // B: 1 2 x 3 x 3 4 + // + // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from + // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 + // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 + // + // The second option is the less one since it hasless segments const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); - if (!sameNodesAheadA.length && !sameNodesAheadB.length) { + if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { indexA = newIndexA; indexB = newIndexB; @@ -193,11 +205,9 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { continue mainLoop; } + // If we have more than one option we need to check all of them out before picking the best one let bestCandidate = CandidateMatch.createEmpty(); - // TODO instead of getBestMatch use exploreMatch so that it skips node if necessary - // TODO also getBestMatch has hardcoded B side - for (const node of sameNodesAheadA) { const newCandidate = getBestMatch(node); @@ -206,13 +216,6 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { } if (newCandidate.isBetterThan(bestCandidate)) { - // Give this input - // A B C C D - // 0 1 2 3 4 - // - // Lets say we need to pick between one of the "C", picking the first one will mean no skip, picking the second one will imply one skip - // 1: A B C - // 2: A B _ C skipsInLookaheadA += node.index - newA.index; bestCandidate = newCandidate; } From a397e7b02b1b901a677a2b229bbce5e56519fe31 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 8 Apr 2024 12:32:35 -0300 Subject: [PATCH 59/79] Wip --- src/v2/core.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/v2/core.ts b/src/v2/core.ts index 2a1f66c..a4965d2 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -54,8 +54,6 @@ export function computeDiff() { // 2- Get all possible matches from subsequence nodes and compare it with the original match found. Pick the best bestMatchForB = getBestMatchFromSubsequenceNodes(bestMatchForB, b); - // bestMatchForB = getBestMatchCheckingBackwards(bestMatchForB, b); - // 3 bestMatchForB = filterOutSingleSingleNodeFromMatch(bestMatchForB); @@ -121,9 +119,6 @@ function canNodeCanBeMatchedAlone(segment: Segment) { return _context.languageConfig.canNodeBeMatchedAlone(node); } -function getBestMatchCheckingBackwards(match: CandidateMatch) { -} - function filterOutSingleSingleNodeFromMatch(match: CandidateMatch) { const { iterA, iterB, additions, deletions } = _context; const remainingSegments = []; From 5e9b48a8c45f70ced57f13898b3c0a2db7269599 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 8 Apr 2024 13:12:22 -0300 Subject: [PATCH 60/79] Initial version of backwards checking working --- src/debug.ts | 2 +- src/v2/diff.ts | 223 +++++++++++++++++++++++++++++++++++++++++++++++- src/v2/match.ts | 4 + 3 files changed, 226 insertions(+), 3 deletions(-) diff --git a/src/debug.ts b/src/debug.ts index 2e3fdf9..b5a1fb2 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -93,7 +93,7 @@ export function createTextTable( colorFn.green("Revision"), ], colAligns: ["left", "left"], - colWidths: [5, 50, 50], + colWidths: [5, 40, 40], style: { compact: true, }, diff --git a/src/v2/diff.ts b/src/v2/diff.ts index a4ab434..d0d970d 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -7,7 +7,7 @@ import { Node } from "./node"; import { equals, getAllNodesFromMatch, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments } from "./utils"; import { Segment } from "./types"; import { CandidateMatch } from "./match"; -import { Side } from "../shared/language"; +import { Direction, Side } from "../shared/language"; import { DiffType } from "../types"; import { trace } from "./tracing"; @@ -65,6 +65,11 @@ function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { const forwardPass = exploreMatchForward(nodeA, nodeB); + const backwardPass = exploreMatchBackward(nodeA, nodeB); + + if (forwardPass.segments[0][0] !== backwardPass.segments[0][0] || forwardPass.segments[0][1] !== backwardPass.segments[0][1]) { + forwardPass.segments[0] = backwardPass.segments[0]; + } return forwardPass } @@ -256,7 +261,221 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { break mainLoop; } - return new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); + const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); + + // _context.matchesCache.add(Side.a, match, Direction.forward); + // _context.matchesCache.add(Side.b, match, Direction.forward); + + return match +} + +function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { + assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); + + const segments: Segment[] = []; + const { iterA, iterB } = _context; + + let segmentLength = 0; + let skips = 0; + + let currentASegmentStart = nodeA.index; + let currentBSegmentStart = nodeB.index; + + let indexA = nodeA.index; + let indexB = nodeB.index; + + assert(equals(nodeA, nodeB), () => "Misaligned matched"); + + mainLoop: while (true) { + // TODO-2 First iteration already has the nodes + const nextA = iterA.peek(indexA); + const nextB = iterB.peek(indexB); + + // If one of the iterators ends then there is no more search to do + if (!nextA || !nextB) { + assert(segmentLength > 0, () => "Segment length is 0"); + + // BACKWARDS since end is excluvive we dont need to deal with reverting the false step, for backwards we need to handle that + + segments.push([ + ++currentASegmentStart, + ++currentBSegmentStart, + segmentLength, + ]); + break mainLoop; + } + + if (equals(nextA, nextB)) { + segmentLength++; + // BACKWARDS + indexA--; + indexB--; + + currentASegmentStart-- + currentBSegmentStart-- + continue; + } + + assert(segmentLength > 0, () => "Segment length is 0"); + + // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment + segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); + segmentLength = 0; + + let skipsInLookaheadB = 0; + + // Verify the following n number of nodes ahead storing the ones equals to the wanted node + // + // A B C B C B B + // 0 1 2 3 4 5 6 + // + // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped + function scanForSameNodes( + iter: Iterator, + wantedNode: Node, + lastIndex: number, + ) { + const nodes: Node[] = []; + // BACKWARDS + for (const index of rangeEq(wantedNode.index - 1, lastIndex)) { + const next = iter.peek(index); + + if (!next) { + continue; + } + + if (equals(wantedNode, next)) { + nodes.push(next); + } + } + return nodes; + } + + // We will skip N nodes on A before skipping 1 node on B, up to N skips in B. N = maxNodeSkips defined in the options + // For example: + // + // A: 1 2 w x y z 4 5 + // B: 1 2 3 4 5 + // + // - We match "1 2", match breaks when we react "3" on B + // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips + // - We skip on B, now it's "4", we start again from the original A position, which is "w" + // - After 4 skips we found a match + // - Final results it's: + // 1 2 _ _ _ _ 4 5 + // 1 2 _ 4 5 + + // BACKWARDS + const skipBUntil = Math.max(iterB.nodes.length, indexB - _context.options.maxNodeSkips); + for (const newIndexB of range(indexB, skipBUntil)) { + const newB = iterB.peek(newIndexB); + + if (!newB) { + break mainLoop; + } + + let skipsInLookaheadA = 0; + + // BACKWARDS + const skipAUntil = Math.max(iterA.nodes.length, indexA - _context.options.maxNodeSkips); + lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { + assert(newB); + + const newA = iterA.peek(newIndexA); + + if (!newA) { + break lookaheadA; + } + + // After the appropriate skips we need to test if we found a match + if (equals(newA, newB)) { + // Because we may had skipped on A and B we need to ensure that the nodes from the new match are the best candidate + // Since there may be same nodes ahead, for example, given: + // + // A: 1 2 3 4 + // B: 1 2 x 3 x 3 4 + // + // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from + // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 + // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 + // + // The second option is the less one since it hasless segments + const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); + const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); + + if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { + indexA = newIndexA; + indexB = newIndexB; + + currentASegmentStart = newIndexA; + currentBSegmentStart = newIndexB; + + skips += skipsInLookaheadA + skipsInLookaheadB; + continue mainLoop; + } + + // If we have more than one option we need to check all of them out before picking the best one + let bestCandidate = CandidateMatch.createEmpty(); + + for (const node of sameNodesAheadA) { + // BACKWARDS forward only? + const newCandidate = getBestMatch(node); + + if (!newCandidate) { + fail(); + } + + if (newCandidate.isBetterThan(bestCandidate)) { + skipsInLookaheadA += node.index - newA.index; + bestCandidate = newCandidate; + } + } + + for (const node of sameNodesAheadB) { + // BACKWARDS forward only? + const newCandidate = getBestMatch(node); + + if (!newCandidate) { + fail(); + } + + if (newCandidate.isBetterThan(bestCandidate)) { + skipsInLookaheadB += node.index - newB.index; + bestCandidate = newCandidate; + } + } + + const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]); + + indexA = a.start; + indexB = b.start; + + currentASegmentStart = a.start; + currentBSegmentStart = b.start; + + skips += skipsInLookaheadA + skipsInLookaheadB; + + continue mainLoop; + } + + skipsInLookaheadA++; + } + + skipsInLookaheadB++; + } + + break mainLoop; + } + + + + // BACKWARDS forward only? + const match = new CandidateMatch(segments.reverse(), getTextLengthFromSegments(segments.reverse()), skips); + + // _context.matchesCache.add(Side.a, match, Direction.forward); + // _context.matchesCache.add(Side.b, match, Direction.forward); + + return match } export function getBestMatchFromSubsequenceNodes(currentBestMatch: CandidateMatch, b: Node) { diff --git a/src/v2/match.ts b/src/v2/match.ts index 669c94f..366a723 100644 --- a/src/v2/match.ts +++ b/src/v2/match.ts @@ -1,3 +1,6 @@ +import { _context } from "."; +import { Change } from "./change"; +import { prettyPrintChanges } from "./debug"; import { Segment } from "./types"; export class CandidateMatch { @@ -28,5 +31,6 @@ export class CandidateMatch { } draw() { + return prettyPrintChanges(_context.sourceA, _context.sourceB, [Change.createMove(this)]) } } From d6cfb697a1c21ee82773353136a7f31b9b9986e1 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 8 Apr 2024 13:55:18 -0300 Subject: [PATCH 61/79] More fixes that break infinite recursion --- src/v2/diff.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index d0d970d..44c2bdc 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -214,7 +214,7 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { let bestCandidate = CandidateMatch.createEmpty(); for (const node of sameNodesAheadA) { - const newCandidate = getBestMatch(node); + const newCandidate = exploreMatchForward(node, newB); if (!newCandidate) { fail(); @@ -227,7 +227,7 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { } for (const node of sameNodesAheadB) { - const newCandidate = getBestMatch(node); + const newCandidate = exploreMatchForward(newA, node); if (!newCandidate) { fail(); @@ -366,7 +366,7 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { // 1 2 _ 4 5 // BACKWARDS - const skipBUntil = Math.max(iterB.nodes.length, indexB - _context.options.maxNodeSkips); + const skipBUntil = Math.max(0, indexB - _context.options.maxNodeSkips); for (const newIndexB of range(indexB, skipBUntil)) { const newB = iterB.peek(newIndexB); @@ -377,7 +377,7 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { let skipsInLookaheadA = 0; // BACKWARDS - const skipAUntil = Math.max(iterA.nodes.length, indexA - _context.options.maxNodeSkips); + const skipAUntil = Math.max(0, indexA - _context.options.maxNodeSkips); lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { assert(newB); From 6f308c98640b7315f87fb36b63e624cbccf209df Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 8 Apr 2024 19:22:10 -0300 Subject: [PATCH 62/79] Working backwards pass --- src/v2/diff.ts | 62 ++++++++++++++------- src/v2/match.ts | 2 +- src/v2/utils.ts | 10 ++-- tests/conformance/sequence-matching.test.ts | 19 ++++++- 4 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 44c2bdc..1303679 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -4,7 +4,7 @@ import { assert, fail } from "../debug"; import { Iterator } from "./iterator"; import { oppositeSide, range, rangeEq } from "../utils"; import { Node } from "./node"; -import { equals, getAllNodesFromMatch, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments } from "./utils"; +import { equals, getAllNodesFromMatch, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments, skipFirstFromArray } from "./utils"; import { Segment } from "./types"; import { CandidateMatch } from "./match"; import { Direction, Side } from "../shared/language"; @@ -63,15 +63,39 @@ function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { return [...allNodes]; } +// This functions is `local` meaning that will move around only a tiny bit forward or backward. +// This is in contrast to the `getBestMatch` fn that scan the whole list of nodes export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { + // We need both forward and backwards passes because our algorithm jumps around, which can leads us into a situation like this + // _____ + // .. 3 4 5 6 7 + // _____ + // 3 4 5 6 7 + // + // Here we started from "5" and matched "5 6 7", but we can extend the match by going backwards. The same skipping logic applies for both directions const forwardPass = exploreMatchForward(nodeA, nodeB); const backwardPass = exploreMatchBackward(nodeA, nodeB); - if (forwardPass.segments[0][0] !== backwardPass.segments[0][0] || forwardPass.segments[0][1] !== backwardPass.segments[0][1]) { - forwardPass.segments[0] = backwardPass.segments[0]; - } + // If we simplify segments we could represent the following state as + // Forward: [5a, 6, 7] + // Backwards: [3, 4, 5b] + + // [3, 4] + const tail = skipFirstFromArray(forwardPass.segments); + // [6, 7] + const head = skipFirstFromArray(backwardPass.segments); + + const forwardMiddle = forwardPass.segments[0]; + const backwardMiddle = backwardPass.segments[0]; + + // [5] + // TODO: Refactor this because the logic from the -1 is confusing + // IF the backwards pass does add anything, it will still have 1 length, so adding that up and doing -1 cancel it out + backwardMiddle[2] += forwardMiddle[2] - 1; - return forwardPass + forwardPass.segments = [...head, backwardMiddle, ...tail]; + + return forwardPass; } function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { @@ -154,7 +178,7 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { // // A: 1 2 w x y z 4 5 // B: 1 2 3 4 5 - // + // // - We match "1 2", match breaks when we react "3" on B // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips // - We skip on B, now it's "4", we start again from the original A position, which is "w" @@ -194,8 +218,8 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 - // - // The second option is the less one since it hasless segments + // + // The second option is the less one since it has less segments const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); @@ -266,7 +290,7 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { // _context.matchesCache.add(Side.a, match, Direction.forward); // _context.matchesCache.add(Side.b, match, Direction.forward); - return match + return match; } function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { @@ -311,15 +335,16 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { indexA--; indexB--; - currentASegmentStart-- - currentBSegmentStart-- + currentASegmentStart--; + currentBSegmentStart--; continue; } assert(segmentLength > 0, () => "Segment length is 0"); // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment - segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); + // BACKWARDs + segments.push([++currentASegmentStart, ++currentBSegmentStart, segmentLength]); segmentLength = 0; let skipsInLookaheadB = 0; @@ -356,7 +381,7 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { // // A: 1 2 w x y z 4 5 // B: 1 2 3 4 5 - // + // // - We match "1 2", match breaks when we react "3" on B // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips // - We skip on B, now it's "4", we start again from the original A position, which is "w" @@ -398,7 +423,7 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 - // + // // The second option is the less one since it hasless segments const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); @@ -467,15 +492,14 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { break mainLoop; } - - - // BACKWARDS forward only? - const match = new CandidateMatch(segments.reverse(), getTextLengthFromSegments(segments.reverse()), skips); + // BACKWARDS + segments.reverse(); + const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); // _context.matchesCache.add(Side.a, match, Direction.forward); // _context.matchesCache.add(Side.b, match, Direction.forward); - return match + return match; } export function getBestMatchFromSubsequenceNodes(currentBestMatch: CandidateMatch, b: Node) { diff --git a/src/v2/match.ts b/src/v2/match.ts index 366a723..c64f823 100644 --- a/src/v2/match.ts +++ b/src/v2/match.ts @@ -31,6 +31,6 @@ export class CandidateMatch { } draw() { - return prettyPrintChanges(_context.sourceA, _context.sourceB, [Change.createMove(this)]) + return prettyPrintChanges(_context.sourceA, _context.sourceB, [Change.createMove(this)]); } } diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 76f59f4..0710bcf 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -96,17 +96,15 @@ export function getTextLengthFromSegments(segments: Segment[]) { return sum; } -export function getAllNodesFromMatch(match: CandidateMatch, side = Side.b) { - const iter = getIterFromSide(side); - +export function getAllNodesFromSegment(iter: Iterator, side: Side, segment: Segment) { const nodes: Node[] = []; for (const segment of match.segments) { const { start, end } = getIndexesFromSegment(segment)[side]; - for (const index of range(start, end)) { - const node = iter.nodes[index]; + for (const index of range(start, end)) { + const node = iter.nodes[index]; - assert(node); + assert(node); nodes.push(node); } diff --git a/tests/conformance/sequence-matching.test.ts b/tests/conformance/sequence-matching.test.ts index 0a39607..9505db2 100644 --- a/tests/conformance/sequence-matching.test.ts +++ b/tests/conformance/sequence-matching.test.ts @@ -161,6 +161,23 @@ describe("Properly report moves in a same sequence", () => { `, }); + test({ + name: "Backwards pass, simple. Adjacent node", + a: ` + 1 2 3 + `, + b: ` + 2 + + 1 2 3 + `, + expB: ` + ➕2➕ + + 1 2 3 + `, + }); + // test({ // name: "Recursive matching 3", // a: ` @@ -221,7 +238,7 @@ describe("Properly report moves in a same sequence", () => { // `, // }); - // // This tests going backward in the LCS calculation + // // This tests going backwards in the LCS calculation // test({ // name: "Recursive matching 5", // a: ` From a2d44bcbba347cee66c4d8d6396a3fb074b3003d Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 9 Apr 2024 11:46:24 -0300 Subject: [PATCH 63/79] Moved code --- src/v2/diff.ts | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 1303679..3ae8b50 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -45,24 +45,6 @@ export function getBestMatch(node: Node): CandidateMatch | undefined { return bestMatch; } -function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { - const nodesInMatch = getAllNodesFromMatch(match); - - const allNodes = new Set(); - - for (const node of nodesInMatch) { - const similarNodes = _context.iterB.getMatchingNodes(node); - - for (const _node of similarNodes) { - allNodes.add(_node); - } - } - - allNodes.delete(starterNode); - - return [...allNodes]; -} - // This functions is `local` meaning that will move around only a tiny bit forward or backward. // This is in contrast to the `getBestMatch` fn that scan the whole list of nodes export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { @@ -540,3 +522,21 @@ export function getBestMatchFromSubsequenceNodes(currentBestMatch: CandidateMatc return bestSubsequenceMatch; } + +function getSubSequenceNodes(match: CandidateMatch, starterNode: Node) { + const nodesInMatch = getAllNodesFromMatch(match); + + const allNodes = new Set(); + + for (const node of nodesInMatch) { + const similarNodes = _context.iterB.getMatchingNodes(node); + + for (const _node of similarNodes) { + allNodes.add(_node); + } + } + + allNodes.delete(starterNode); + + return [...allNodes]; +} From af5ac4403b6edca621665100be7c0adc7221ad8d Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 10 Apr 2024 17:35:26 -0300 Subject: [PATCH 64/79] Unit tests passing --- src/v2/diff.ts | 99 +++++++++------ src/v2/utils.ts | 9 ++ tests/conformance/backwards-pass.test.ts | 130 ++++++++++++++++++++ tests/conformance/sequence-matching.test.ts | 17 --- 4 files changed, 201 insertions(+), 54 deletions(-) create mode 100644 tests/conformance/backwards-pass.test.ts diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 3ae8b50..4ac4bf5 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -4,7 +4,7 @@ import { assert, fail } from "../debug"; import { Iterator } from "./iterator"; import { oppositeSide, range, rangeEq } from "../utils"; import { Node } from "./node"; -import { equals, getAllNodesFromMatch, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments, skipFirstFromArray } from "./utils"; +import { equals, getAllNodesFromMatch, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments, skipFirstFromArray, skipLastFromArray } from "./utils"; import { Segment } from "./types"; import { CandidateMatch } from "./match"; import { Direction, Side } from "../shared/language"; @@ -45,7 +45,7 @@ export function getBestMatch(node: Node): CandidateMatch | undefined { return bestMatch; } -// This functions is `local` meaning that will move around only a tiny bit forward or backward. +// This functions is `local` meaning that will move around only a tiny bit forward or backward. // This is in contrast to the `getBestMatch` fn that scan the whole list of nodes export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { // We need both forward and backwards passes because our algorithm jumps around, which can leads us into a situation like this @@ -58,25 +58,38 @@ export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { const forwardPass = exploreMatchForward(nodeA, nodeB); const backwardPass = exploreMatchBackward(nodeA, nodeB); - // If we simplify segments we could represent the following state as - // Forward: [5a, 6, 7] - // Backwards: [3, 4, 5b] - - // [3, 4] - const tail = skipFirstFromArray(forwardPass.segments); - // [6, 7] - const head = skipFirstFromArray(backwardPass.segments); - - const forwardMiddle = forwardPass.segments[0]; - const backwardMiddle = backwardPass.segments[0]; - - // [5] - // TODO: Refactor this because the logic from the -1 is confusing - // IF the backwards pass does add anything, it will still have 1 length, so adding that up and doing -1 cancel it out - backwardMiddle[2] += forwardMiddle[2] - 1; + if (backwardPass) { + trace("We can extend the match by going backward", backwardPass); + + // We need to merge the two matches, we could represent the current state like follows + // Forward: [5a, 6, 7] + // Backwards: [3, 4, 5b] + + const forwardIndexes = getIndexesFromSegment(forwardPass.segments.at(0)!); + const backwardsIndexes = getIndexesFromSegment(backwardPass.segments.at(-1)!); + + // If the two segments are in sequences, like [3, 4] and [4, 5] we need to combine them into a single segment + if (forwardIndexes.a.start === backwardsIndexes.a.end && forwardIndexes.b.start === backwardsIndexes.b.end) { + // Following the example above this would be [3, 4] + const tail = skipFirstFromArray(forwardPass.segments); + // Following the example above this would be [6, 7] + const head = skipLastFromArray(backwardPass.segments); + + // Now we need to merge 5a and 5b + const middle: Segment = [ + backwardsIndexes.a.start, + backwardsIndexes.b.start, + forwardIndexes.length + backwardsIndexes.length, + ]; + + forwardPass.segments = [...head, middle, ...tail]; + forwardPass.textLength = getTextLengthFromSegments(forwardPass.segments); + } else { + // If the matches are not continuos, such as [1, 2] and [3, 4] there is no extra work to do + forwardPass.segments = [...backwardPass.segments, ...forwardPass.segments]; + } + } - forwardPass.segments = [...head, backwardMiddle, ...tail]; - return forwardPass; } @@ -275,7 +288,7 @@ function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { return match; } -function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { +function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefined { assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); const segments: Segment[] = []; @@ -284,11 +297,13 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { let segmentLength = 0; let skips = 0; + // Arranco en -1 porque no quiero devolver lo que ya esta matcheado, en esta funcion puedo devolver undefined + let currentASegmentStart = nodeA.index; let currentBSegmentStart = nodeB.index; - let indexA = nodeA.index; - let indexB = nodeB.index; + let indexA = nodeA.index - 1; + let indexB = nodeB.index - 1; assert(equals(nodeA, nodeB), () => "Misaligned matched"); @@ -299,13 +314,15 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { // If one of the iterators ends then there is no more search to do if (!nextA || !nextB) { - assert(segmentLength > 0, () => "Segment length is 0"); + if (segmentLength === 0) { + return; + } // BACKWARDS since end is excluvive we dont need to deal with reverting the false step, for backwards we need to handle that segments.push([ - ++currentASegmentStart, - ++currentBSegmentStart, + currentASegmentStart - segmentLength, + currentBSegmentStart - segmentLength, segmentLength, ]); break mainLoop; @@ -316,17 +333,21 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { // BACKWARDS indexA--; indexB--; - - currentASegmentStart--; - currentBSegmentStart--; continue; } - assert(segmentLength > 0, () => "Segment length is 0"); - // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment // BACKWARDs - segments.push([++currentASegmentStart, ++currentBSegmentStart, segmentLength]); + + // nada me garantiza que halla algo para pushear porque arrancamos en -1 + if (segmentLength) { + segments.push([ + currentASegmentStart - segmentLength, + currentBSegmentStart - segmentLength, + segmentLength, + ]); + } + segmentLength = 0; let skipsInLookaheadB = 0; @@ -344,7 +365,8 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { ) { const nodes: Node[] = []; // BACKWARDS - for (const index of rangeEq(wantedNode.index - 1, lastIndex)) { + // for (const index of rangeEq(wantedNode.index - 1, lastIndex)) { + for (let index = wantedNode.index - 1; index >= lastIndex; index--) { const next = iter.peek(index); if (!next) { @@ -374,7 +396,9 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { // BACKWARDS const skipBUntil = Math.max(0, indexB - _context.options.maxNodeSkips); - for (const newIndexB of range(indexB, skipBUntil)) { + // for (const newIndexB of range(indexB, skipBUntil)) { + for (let newIndexB = indexB; newIndexB >= skipBUntil; newIndexB--) { + // for (const newIndexB of range(indexB, skipBUntil, i => i - 1)) { const newB = iterB.peek(newIndexB); if (!newB) { @@ -385,7 +409,8 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { // BACKWARDS const skipAUntil = Math.max(0, indexA - _context.options.maxNodeSkips); - lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { + lookaheadA: for (let newIndexA = indexA; newIndexA >= skipAUntil; newIndexA--) { + // lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { assert(newB); const newA = iterA.peek(newIndexA); @@ -414,8 +439,8 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch { indexA = newIndexA; indexB = newIndexB; - currentASegmentStart = newIndexA; - currentBSegmentStart = newIndexB; + currentASegmentStart = newIndexA + 1; + currentBSegmentStart = newIndexB + 1; skips += skipsInLookaheadA + skipsInLookaheadB; continue mainLoop; diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 0710bcf..633741a 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -129,6 +129,7 @@ export function getIndexesFromSegment(segment: Segment) { start: startB, end: startB + length, }, + length, }; } @@ -170,3 +171,11 @@ export const sort = { export function matchContainsSingleNode(match: CandidateMatch) { return match.segments.length === 1 && match.segments[0][2] === 1; } + +export function skipFirstFromArray(array: T[]): T[] { + return array.slice(1); +} + +export function skipLastFromArray(array: T[]): T[] { + return array.slice(0, -1); +} diff --git a/tests/conformance/backwards-pass.test.ts b/tests/conformance/backwards-pass.test.ts new file mode 100644 index 0000000..8e270df --- /dev/null +++ b/tests/conformance/backwards-pass.test.ts @@ -0,0 +1,130 @@ +import { test } from "../utils2"; + +// Testing the `exploreBackwards` fn. Since we always look on B to find what to match we find "6", this give us the "6 7" match +// the rest of the match needs to be discovered by going backwards + +// From "6 7" we add "5" which is next to it +test({ + name: 'One node adjacent', + a: ` + 5 6 7 + `, + b: ` + 6 + + 5 6 7 + `, + expB: ` + ➕6➕ + + 5 6 7 + `, +}); + +// From "6 7" we add "4 5" which are next to it +test({ + name: 'Two nodes adjacent', + a: ` + 4 5 6 7 + `, + b: ` + 6 + + 4 5 6 7 + `, + expB: ` + ➕6➕ + + 4 5 6 7 + `, +}); + +// From "6 7" we add "5" after one skip +test({ + name: "One node after skip", + a: ` + 5 6 7 + `, + b: ` + 6 + + 5 x 6 7 + `, + expB: ` + ➕6➕ + + 5 ➕x➕ 6 7 + `, +}); + +// From "6 7" we add "4 5" after one skip +test({ + name: "Two node after skip", + a: ` + 4 5 6 7 + `, + b: ` + 6 + + 4 5 x 6 7 + `, + expB: ` + ➕6➕ + + 4 5 ➕x➕ 6 7 + `, +}); + +// From "6 7" we add "5" after one skip and "4" after another skip +test({ + name: "One node one skip, two times", + a: ` + 4 5 6 7 + `, + b: ` + 6 + + 4 x 5 x 6 7 + `, + expB: ` + ➕6➕ + + 4 ➕x➕ 5 ➕x➕ 6 7 + `, +}); + +// More versions of the above mentioned cases + +test({ + name: "Extra case 1", + a: ` + 3 4 5 6 7 + `, + b: ` + 6 + + 3 4 x 5 x 6 7 + `, + expB: ` + ➕6➕ + + 3 4 ➕x➕ 5 ➕x➕ 6 7 + `, +}); + +test({ + name: "Extra case 2", + a: ` + 3 4 5 6 7 + `, + b: ` + 6 + + 3 x 4 5 x 6 7 + `, + expB: ` + ➕6➕ + + 3 ➕x➕ 4 5 ➕x➕ 6 7 + `, +}); diff --git a/tests/conformance/sequence-matching.test.ts b/tests/conformance/sequence-matching.test.ts index 9505db2..9222b77 100644 --- a/tests/conformance/sequence-matching.test.ts +++ b/tests/conformance/sequence-matching.test.ts @@ -161,23 +161,6 @@ describe("Properly report moves in a same sequence", () => { `, }); - test({ - name: "Backwards pass, simple. Adjacent node", - a: ` - 1 2 3 - `, - b: ` - 2 - - 1 2 3 - `, - expB: ` - ➕2➕ - - 1 2 3 - `, - }); - // test({ // name: "Recursive matching 3", // a: ` From 01993182521afbc2412e3edcfcf6d39e7ebe7332 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 10 Apr 2024 18:13:45 -0300 Subject: [PATCH 65/79] Small hack to improve the debugging experience --- src/v2/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/v2/index.ts b/src/v2/index.ts index 8fe63d2..e86d600 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -9,6 +9,12 @@ import { Move } from "./change"; import { computeMoveAlignment } from "./semanticAligment"; import { getAdditionsAndDeletionsChanges } from "./compact"; import { trace } from "./tracing"; +import * as debug from "./debug"; + +declare global { + var debug: typeof import("./debug"); +} +globalThis.debug = debug; export function getDiff2<_OutputType extends OutputType = OutputType.changes>( sourceA: string, From ee7d9a680c4316e9dc1c6eb3ac76c3181d932883 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 10 Apr 2024 18:14:01 -0300 Subject: [PATCH 66/79] Fix crash in backwards exploration --- src/v2/diff.ts | 5 +++++ tests/conformance/backwards-pass.test.ts | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index 4ac4bf5..c491ffa 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -496,6 +496,11 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi skipsInLookaheadB++; } + // Backwards only + if (segments.length === 0) { + return + } + break mainLoop; } diff --git a/tests/conformance/backwards-pass.test.ts b/tests/conformance/backwards-pass.test.ts index 8e270df..43abc5d 100644 --- a/tests/conformance/backwards-pass.test.ts +++ b/tests/conformance/backwards-pass.test.ts @@ -128,3 +128,24 @@ test({ 3 ➕x➕ 4 5 ➕x➕ 6 7 `, }); + +// Used to crash +test({ + name: "Extra case 3", + a: ` + a b 6 7 + `, + b: ` + 6 + + x z 6 7 + `, + expA: ` + ➖a b➖ 6 7 + `, + expB: ` + ➕6 + + x z➕ 6 7 + `, +}); From 9006394583dd02318e4a7a3ea2f982188880390d Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 10 Apr 2024 18:14:18 -0300 Subject: [PATCH 67/79] Refactor util --- src/v2/utils.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/v2/utils.ts b/src/v2/utils.ts index 633741a..fa07bea 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -98,16 +98,26 @@ export function getTextLengthFromSegments(segments: Segment[]) { export function getAllNodesFromSegment(iter: Iterator, side: Side, segment: Segment) { const nodes: Node[] = []; - for (const segment of match.segments) { - const { start, end } = getIndexesFromSegment(segment)[side]; + + const { start, end } = getIndexesFromSegment(segment)[side]; for (const index of range(start, end)) { const node = iter.nodes[index]; assert(node); - nodes.push(node); - } + nodes.push(node); + } + + return nodes; +} + +export function getAllNodesFromMatch(match: CandidateMatch, side = Side.b) { + const iter = getIterFromSide(side); + + const nodes: Node[] = []; + for (const segment of match.segments) { + nodes.push(...getAllNodesFromSegment(iter, side, segment)); } return nodes; From 631d09cd63459c1c3a7429627c539f8f305ad3a1 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 10 Apr 2024 18:19:07 -0300 Subject: [PATCH 68/79] Improved test --- tests/conformance/move.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conformance/move.test.ts b/tests/conformance/move.test.ts index 9cb7849..3438150 100644 --- a/tests/conformance/move.test.ts +++ b/tests/conformance/move.test.ts @@ -109,10 +109,10 @@ describe("Properly report lines added", () => { `, expB: ` x - 1 - 2 ➕1 2➕ + 1 + 2 3 `, }); From 83dfd7b3f5ef0424548f78771763175b6945d441 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Wed, 10 Apr 2024 18:41:13 -0300 Subject: [PATCH 69/79] Fix bug --- src/v2/diff.ts | 25 +++++++++++++++++++------ src/v2/match.ts | 4 ++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index c491ffa..d50016d 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -436,6 +436,7 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { + // RESUME indexA = newIndexA; indexB = newIndexB; @@ -450,11 +451,11 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi let bestCandidate = CandidateMatch.createEmpty(); for (const node of sameNodesAheadA) { - // BACKWARDS forward only? - const newCandidate = getBestMatch(node); + const newCandidate = exploreMatchBackward(node, newB); if (!newCandidate) { - fail(); + // BACKWARDS + continue } if (newCandidate.isBetterThan(bestCandidate)) { @@ -464,11 +465,11 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi } for (const node of sameNodesAheadB) { - // BACKWARDS forward only? - const newCandidate = getBestMatch(node); + const newCandidate = exploreMatchBackward(newA, node); if (!newCandidate) { - fail(); + // BACKWARDS + continue } if (newCandidate.isBetterThan(bestCandidate)) { @@ -477,6 +478,18 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi } } + if (bestCandidate.isEmpty()) { + // RESUME + indexA = newIndexA; + indexB = newIndexB; + + currentASegmentStart = newIndexA + 1; + currentBSegmentStart = newIndexB + 1; + + skips += skipsInLookaheadA + skipsInLookaheadB; + continue mainLoop; + } + const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]); indexA = a.start; diff --git a/src/v2/match.ts b/src/v2/match.ts index c64f823..260a25e 100644 --- a/src/v2/match.ts +++ b/src/v2/match.ts @@ -30,6 +30,10 @@ export class CandidateMatch { return this.skips < otherCandidate.skips; } + isEmpty() { + return this.segments.length === 0 + } + draw() { return prettyPrintChanges(_context.sourceA, _context.sourceB, [Change.createMove(this)]); } From 226ee2be4003a510c6ff2252d8b0e4fef491667f Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Fri, 12 Apr 2024 18:08:49 -0300 Subject: [PATCH 70/79] Moved code --- src/v2/diff.ts | 492 +---------------------------------------- src/v2/exploreMatch.ts | 491 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 495 insertions(+), 488 deletions(-) create mode 100644 src/v2/exploreMatch.ts diff --git a/src/v2/diff.ts b/src/v2/diff.ts index d50016d..fbef915 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -1,15 +1,13 @@ import colorFn from "kleur"; import { _context } from "."; -import { assert, fail } from "../debug"; -import { Iterator } from "./iterator"; -import { oppositeSide, range, rangeEq } from "../utils"; +import { oppositeSide } from "../utils"; import { Node } from "./node"; -import { equals, getAllNodesFromMatch, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments, skipFirstFromArray, skipLastFromArray } from "./utils"; -import { Segment } from "./types"; +import { getAllNodesFromMatch, getIterFromSide } from "./utils"; import { CandidateMatch } from "./match"; -import { Direction, Side } from "../shared/language"; +import { Side } from "../shared/language"; import { DiffType } from "../types"; import { trace } from "./tracing"; +import { exploreMatch } from "./exploreMatch"; /** * Returns the longest possible match for the given node, this is including possible skips to improve the match length. @@ -45,488 +43,6 @@ export function getBestMatch(node: Node): CandidateMatch | undefined { return bestMatch; } -// This functions is `local` meaning that will move around only a tiny bit forward or backward. -// This is in contrast to the `getBestMatch` fn that scan the whole list of nodes -export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { - // We need both forward and backwards passes because our algorithm jumps around, which can leads us into a situation like this - // _____ - // .. 3 4 5 6 7 - // _____ - // 3 4 5 6 7 - // - // Here we started from "5" and matched "5 6 7", but we can extend the match by going backwards. The same skipping logic applies for both directions - const forwardPass = exploreMatchForward(nodeA, nodeB); - const backwardPass = exploreMatchBackward(nodeA, nodeB); - - if (backwardPass) { - trace("We can extend the match by going backward", backwardPass); - - // We need to merge the two matches, we could represent the current state like follows - // Forward: [5a, 6, 7] - // Backwards: [3, 4, 5b] - - const forwardIndexes = getIndexesFromSegment(forwardPass.segments.at(0)!); - const backwardsIndexes = getIndexesFromSegment(backwardPass.segments.at(-1)!); - - // If the two segments are in sequences, like [3, 4] and [4, 5] we need to combine them into a single segment - if (forwardIndexes.a.start === backwardsIndexes.a.end && forwardIndexes.b.start === backwardsIndexes.b.end) { - // Following the example above this would be [3, 4] - const tail = skipFirstFromArray(forwardPass.segments); - // Following the example above this would be [6, 7] - const head = skipLastFromArray(backwardPass.segments); - - // Now we need to merge 5a and 5b - const middle: Segment = [ - backwardsIndexes.a.start, - backwardsIndexes.b.start, - forwardIndexes.length + backwardsIndexes.length, - ]; - - forwardPass.segments = [...head, middle, ...tail]; - forwardPass.textLength = getTextLengthFromSegments(forwardPass.segments); - } else { - // If the matches are not continuos, such as [1, 2] and [3, 4] there is no extra work to do - forwardPass.segments = [...backwardPass.segments, ...forwardPass.segments]; - } - } - - return forwardPass; -} - -function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { - assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); - - const segments: Segment[] = []; - const { iterA, iterB } = _context; - - let segmentLength = 0; - let skips = 0; - - let currentASegmentStart = nodeA.index; - let currentBSegmentStart = nodeB.index; - - let indexA = nodeA.index; - let indexB = nodeB.index; - - assert(equals(nodeA, nodeB), () => "Misaligned matched"); - - mainLoop: while (true) { - // TODO-2 First iteration already has the nodes - const nextA = iterA.peek(indexA); - const nextB = iterB.peek(indexB); - - // If one of the iterators ends then there is no more search to do - if (!nextA || !nextB) { - assert(segmentLength > 0, () => "Segment length is 0"); - - segments.push([ - currentASegmentStart, - currentBSegmentStart, - segmentLength, - ]); - break mainLoop; - } - - if (equals(nextA, nextB)) { - segmentLength++; - indexA++; - indexB++; - continue; - } - - assert(segmentLength > 0, () => "Segment length is 0"); - - // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment - segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); - segmentLength = 0; - - let skipsInLookaheadB = 0; - - // Verify the following n number of nodes ahead storing the ones equals to the wanted node - // - // A B C B C B B - // 0 1 2 3 4 5 6 - // - // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped - function scanForSameNodes( - iter: Iterator, - wantedNode: Node, - lastIndex: number, - ) { - const nodes: Node[] = []; - for (const index of rangeEq(wantedNode.index + 1, lastIndex)) { - const next = iter.peek(index); - - if (!next) { - continue; - } - - if (equals(wantedNode, next)) { - nodes.push(next); - } - } - return nodes; - } - - // We will skip N nodes on A before skipping 1 node on B, up to N skips in B. N = maxNodeSkips defined in the options - // For example: - // - // A: 1 2 w x y z 4 5 - // B: 1 2 3 4 5 - // - // - We match "1 2", match breaks when we react "3" on B - // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips - // - We skip on B, now it's "4", we start again from the original A position, which is "w" - // - After 4 skips we found a match - // - Final results it's: - // 1 2 _ _ _ _ 4 5 - // 1 2 _ 4 5 - - const skipBUntil = Math.min(iterB.nodes.length, indexB + _context.options.maxNodeSkips); - for (const newIndexB of range(indexB, skipBUntil)) { - const newB = iterB.peek(newIndexB); - - if (!newB) { - break mainLoop; - } - - let skipsInLookaheadA = 0; - - const skipAUntil = Math.min(iterA.nodes.length, indexA + _context.options.maxNodeSkips); - lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { - assert(newB); - - const newA = iterA.peek(newIndexA); - - if (!newA) { - break lookaheadA; - } - - // After the appropriate skips we need to test if we found a match - if (equals(newA, newB)) { - // Because we may had skipped on A and B we need to ensure that the nodes from the new match are the best candidate - // Since there may be same nodes ahead, for example, given: - // - // A: 1 2 3 4 - // B: 1 2 x 3 x 3 4 - // - // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from - // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 - // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 - // - // The second option is the less one since it has less segments - const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); - const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); - - if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { - indexA = newIndexA; - indexB = newIndexB; - - currentASegmentStart = newIndexA; - currentBSegmentStart = newIndexB; - - skips += skipsInLookaheadA + skipsInLookaheadB; - continue mainLoop; - } - - // If we have more than one option we need to check all of them out before picking the best one - let bestCandidate = CandidateMatch.createEmpty(); - - for (const node of sameNodesAheadA) { - const newCandidate = exploreMatchForward(node, newB); - - if (!newCandidate) { - fail(); - } - - if (newCandidate.isBetterThan(bestCandidate)) { - skipsInLookaheadA += node.index - newA.index; - bestCandidate = newCandidate; - } - } - - for (const node of sameNodesAheadB) { - const newCandidate = exploreMatchForward(newA, node); - - if (!newCandidate) { - fail(); - } - - if (newCandidate.isBetterThan(bestCandidate)) { - skipsInLookaheadB += node.index - newB.index; - bestCandidate = newCandidate; - } - } - - const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]); - - indexA = a.start; - indexB = b.start; - - currentASegmentStart = a.start; - currentBSegmentStart = b.start; - - skips += skipsInLookaheadA + skipsInLookaheadB; - - continue mainLoop; - } - - skipsInLookaheadA++; - } - - skipsInLookaheadB++; - } - - break mainLoop; - } - - const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); - - // _context.matchesCache.add(Side.a, match, Direction.forward); - // _context.matchesCache.add(Side.b, match, Direction.forward); - - return match; -} - -function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefined { - assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); - - const segments: Segment[] = []; - const { iterA, iterB } = _context; - - let segmentLength = 0; - let skips = 0; - - // Arranco en -1 porque no quiero devolver lo que ya esta matcheado, en esta funcion puedo devolver undefined - - let currentASegmentStart = nodeA.index; - let currentBSegmentStart = nodeB.index; - - let indexA = nodeA.index - 1; - let indexB = nodeB.index - 1; - - assert(equals(nodeA, nodeB), () => "Misaligned matched"); - - mainLoop: while (true) { - // TODO-2 First iteration already has the nodes - const nextA = iterA.peek(indexA); - const nextB = iterB.peek(indexB); - - // If one of the iterators ends then there is no more search to do - if (!nextA || !nextB) { - if (segmentLength === 0) { - return; - } - - // BACKWARDS since end is excluvive we dont need to deal with reverting the false step, for backwards we need to handle that - - segments.push([ - currentASegmentStart - segmentLength, - currentBSegmentStart - segmentLength, - segmentLength, - ]); - break mainLoop; - } - - if (equals(nextA, nextB)) { - segmentLength++; - // BACKWARDS - indexA--; - indexB--; - continue; - } - - // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment - // BACKWARDs - - // nada me garantiza que halla algo para pushear porque arrancamos en -1 - if (segmentLength) { - segments.push([ - currentASegmentStart - segmentLength, - currentBSegmentStart - segmentLength, - segmentLength, - ]); - } - - segmentLength = 0; - - let skipsInLookaheadB = 0; - - // Verify the following n number of nodes ahead storing the ones equals to the wanted node - // - // A B C B C B B - // 0 1 2 3 4 5 6 - // - // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped - function scanForSameNodes( - iter: Iterator, - wantedNode: Node, - lastIndex: number, - ) { - const nodes: Node[] = []; - // BACKWARDS - // for (const index of rangeEq(wantedNode.index - 1, lastIndex)) { - for (let index = wantedNode.index - 1; index >= lastIndex; index--) { - const next = iter.peek(index); - - if (!next) { - continue; - } - - if (equals(wantedNode, next)) { - nodes.push(next); - } - } - return nodes; - } - - // We will skip N nodes on A before skipping 1 node on B, up to N skips in B. N = maxNodeSkips defined in the options - // For example: - // - // A: 1 2 w x y z 4 5 - // B: 1 2 3 4 5 - // - // - We match "1 2", match breaks when we react "3" on B - // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips - // - We skip on B, now it's "4", we start again from the original A position, which is "w" - // - After 4 skips we found a match - // - Final results it's: - // 1 2 _ _ _ _ 4 5 - // 1 2 _ 4 5 - - // BACKWARDS - const skipBUntil = Math.max(0, indexB - _context.options.maxNodeSkips); - // for (const newIndexB of range(indexB, skipBUntil)) { - for (let newIndexB = indexB; newIndexB >= skipBUntil; newIndexB--) { - // for (const newIndexB of range(indexB, skipBUntil, i => i - 1)) { - const newB = iterB.peek(newIndexB); - - if (!newB) { - break mainLoop; - } - - let skipsInLookaheadA = 0; - - // BACKWARDS - const skipAUntil = Math.max(0, indexA - _context.options.maxNodeSkips); - lookaheadA: for (let newIndexA = indexA; newIndexA >= skipAUntil; newIndexA--) { - // lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { - assert(newB); - - const newA = iterA.peek(newIndexA); - - if (!newA) { - break lookaheadA; - } - - // After the appropriate skips we need to test if we found a match - if (equals(newA, newB)) { - // Because we may had skipped on A and B we need to ensure that the nodes from the new match are the best candidate - // Since there may be same nodes ahead, for example, given: - // - // A: 1 2 3 4 - // B: 1 2 x 3 x 3 4 - // - // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from - // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 - // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 - // - // The second option is the less one since it hasless segments - const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); - const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); - - if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { - // RESUME - indexA = newIndexA; - indexB = newIndexB; - - currentASegmentStart = newIndexA + 1; - currentBSegmentStart = newIndexB + 1; - - skips += skipsInLookaheadA + skipsInLookaheadB; - continue mainLoop; - } - - // If we have more than one option we need to check all of them out before picking the best one - let bestCandidate = CandidateMatch.createEmpty(); - - for (const node of sameNodesAheadA) { - const newCandidate = exploreMatchBackward(node, newB); - - if (!newCandidate) { - // BACKWARDS - continue - } - - if (newCandidate.isBetterThan(bestCandidate)) { - skipsInLookaheadA += node.index - newA.index; - bestCandidate = newCandidate; - } - } - - for (const node of sameNodesAheadB) { - const newCandidate = exploreMatchBackward(newA, node); - - if (!newCandidate) { - // BACKWARDS - continue - } - - if (newCandidate.isBetterThan(bestCandidate)) { - skipsInLookaheadB += node.index - newB.index; - bestCandidate = newCandidate; - } - } - - if (bestCandidate.isEmpty()) { - // RESUME - indexA = newIndexA; - indexB = newIndexB; - - currentASegmentStart = newIndexA + 1; - currentBSegmentStart = newIndexB + 1; - - skips += skipsInLookaheadA + skipsInLookaheadB; - continue mainLoop; - } - - const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]); - - indexA = a.start; - indexB = b.start; - - currentASegmentStart = a.start; - currentBSegmentStart = b.start; - - skips += skipsInLookaheadA + skipsInLookaheadB; - - continue mainLoop; - } - - skipsInLookaheadA++; - } - - skipsInLookaheadB++; - } - - // Backwards only - if (segments.length === 0) { - return - } - - break mainLoop; - } - - // BACKWARDS - segments.reverse(); - const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); - - // _context.matchesCache.add(Side.a, match, Direction.forward); - // _context.matchesCache.add(Side.b, match, Direction.forward); - - return match; -} - export function getBestMatchFromSubsequenceNodes(currentBestMatch: CandidateMatch, b: Node) { const { iterB, additions } = _context; diff --git a/src/v2/exploreMatch.ts b/src/v2/exploreMatch.ts new file mode 100644 index 0000000..70ac488 --- /dev/null +++ b/src/v2/exploreMatch.ts @@ -0,0 +1,491 @@ +import { Node } from "./node"; +import { Iterator } from "./iterator"; +import { _context } from "."; +import { assert, fail } from "../debug"; +import { Side } from "../shared/language"; +import { range, rangeEq } from "../utils"; +import { CandidateMatch } from "./match"; +import { trace } from "./tracing"; +import { Segment } from "./types"; +import { equals, getIndexesFromSegment, getTextLengthFromSegments, skipFirstFromArray, skipLastFromArray } from "./utils"; + +// This functions is `local` meaning that will move around only a tiny bit forward or backward. +// This is in contrast to the `getBestMatch` fn that scan the whole list of nodes +export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { + // We need both forward and backwards passes because our algorithm jumps around, which can leads us into a situation like this + // _____ + // .. 3 4 5 6 7 + // _____ + // 3 4 5 6 7 + // + // Here we started from "5" and matched "5 6 7", but we can extend the match by going backwards. The same skipping logic applies for both directions + const forwardPass = exploreMatchForward(nodeA, nodeB); + const backwardPass = exploreMatchBackward(nodeA, nodeB); + + if (backwardPass) { + trace("We can extend the match by going backward", backwardPass); + + // We need to merge the two matches, we could represent the current state like follows + // Forward: [5a, 6, 7] + // Backwards: [3, 4, 5b] + + const forwardIndexes = getIndexesFromSegment(forwardPass.segments.at(0)!); + const backwardsIndexes = getIndexesFromSegment(backwardPass.segments.at(-1)!); + + // If the two segments are in sequences, like [3, 4] and [4, 5] we need to combine them into a single segment + if (forwardIndexes.a.start === backwardsIndexes.a.end && forwardIndexes.b.start === backwardsIndexes.b.end) { + // Following the example above this would be [3, 4] + const tail = skipFirstFromArray(forwardPass.segments); + // Following the example above this would be [6, 7] + const head = skipLastFromArray(backwardPass.segments); + + // Now we need to merge 5a and 5b + const middle: Segment = [ + backwardsIndexes.a.start, + backwardsIndexes.b.start, + forwardIndexes.length + backwardsIndexes.length, + ]; + + forwardPass.segments = [...head, middle, ...tail]; + forwardPass.textLength = getTextLengthFromSegments(forwardPass.segments); + } else { + // If the matches are not continuos, such as [1, 2] and [3, 4] there is no extra work to do + forwardPass.segments = [...backwardPass.segments, ...forwardPass.segments]; + } + } + + return forwardPass; +} + +function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { + assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); + + const segments: Segment[] = []; + const { iterA, iterB } = _context; + + let segmentLength = 0; + let skips = 0; + + let currentASegmentStart = nodeA.index; + let currentBSegmentStart = nodeB.index; + + let indexA = nodeA.index; + let indexB = nodeB.index; + + assert(equals(nodeA, nodeB), () => "Misaligned matched"); + + mainLoop: while (true) { + // TODO-2 First iteration already has the nodes + const nextA = iterA.peek(indexA); + const nextB = iterB.peek(indexB); + + // If one of the iterators ends then there is no more search to do + if (!nextA || !nextB) { + assert(segmentLength > 0, () => "Segment length is 0"); + + segments.push([ + currentASegmentStart, + currentBSegmentStart, + segmentLength, + ]); + break mainLoop; + } + + if (equals(nextA, nextB)) { + segmentLength++; + indexA++; + indexB++; + continue; + } + + assert(segmentLength > 0, () => "Segment length is 0"); + + // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment + segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); + segmentLength = 0; + + let skipsInLookaheadB = 0; + + // Verify the following n number of nodes ahead storing the ones equals to the wanted node + // + // A B C B C B B + // 0 1 2 3 4 5 6 + // + // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped + function scanForSameNodes( + iter: Iterator, + wantedNode: Node, + lastIndex: number, + ) { + const nodes: Node[] = []; + for (const index of rangeEq(wantedNode.index + 1, lastIndex)) { + const next = iter.peek(index); + + if (!next) { + continue; + } + + if (equals(wantedNode, next)) { + nodes.push(next); + } + } + return nodes; + } + + // We will skip N nodes on A before skipping 1 node on B, up to N skips in B. N = maxNodeSkips defined in the options + // For example: + // + // A: 1 2 w x y z 4 5 + // B: 1 2 3 4 5 + // + // - We match "1 2", match breaks when we react "3" on B + // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips + // - We skip on B, now it's "4", we start again from the original A position, which is "w" + // - After 4 skips we found a match + // - Final results it's: + // 1 2 _ _ _ _ 4 5 + // 1 2 _ 4 5 + + const skipBUntil = Math.min(iterB.nodes.length, indexB + _context.options.maxNodeSkips); + for (const newIndexB of range(indexB, skipBUntil)) { + const newB = iterB.peek(newIndexB); + + if (!newB) { + break mainLoop; + } + + let skipsInLookaheadA = 0; + + const skipAUntil = Math.min(iterA.nodes.length, indexA + _context.options.maxNodeSkips); + lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { + assert(newB); + + const newA = iterA.peek(newIndexA); + + if (!newA) { + break lookaheadA; + } + + // After the appropriate skips we need to test if we found a match + if (equals(newA, newB)) { + // Because we may had skipped on A and B we need to ensure that the nodes from the new match are the best candidate + // Since there may be same nodes ahead, for example, given: + // + // A: 1 2 3 4 + // B: 1 2 x 3 x 3 4 + // + // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from + // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 + // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 + // + // The second option is the less one since it has less segments + const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); + const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); + + if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { + indexA = newIndexA; + indexB = newIndexB; + + currentASegmentStart = newIndexA; + currentBSegmentStart = newIndexB; + + skips += skipsInLookaheadA + skipsInLookaheadB; + continue mainLoop; + } + + // If we have more than one option we need to check all of them out before picking the best one + let bestCandidate = CandidateMatch.createEmpty(); + + for (const node of sameNodesAheadA) { + const newCandidate = exploreMatchForward(node, newB); + + if (!newCandidate) { + fail(); + } + + if (newCandidate.isBetterThan(bestCandidate)) { + skipsInLookaheadA += node.index - newA.index; + bestCandidate = newCandidate; + } + } + + for (const node of sameNodesAheadB) { + const newCandidate = exploreMatchForward(newA, node); + + if (!newCandidate) { + fail(); + } + + if (newCandidate.isBetterThan(bestCandidate)) { + skipsInLookaheadB += node.index - newB.index; + bestCandidate = newCandidate; + } + } + + const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]); + + indexA = a.start; + indexB = b.start; + + currentASegmentStart = a.start; + currentBSegmentStart = b.start; + + skips += skipsInLookaheadA + skipsInLookaheadB; + + continue mainLoop; + } + + skipsInLookaheadA++; + } + + skipsInLookaheadB++; + } + + break mainLoop; + } + + const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); + + // _context.matchesCache.add(Side.a, match, Direction.forward); + // _context.matchesCache.add(Side.b, match, Direction.forward); + + return match; +} + +function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefined { + assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); + + const segments: Segment[] = []; + const { iterA, iterB } = _context; + + let segmentLength = 0; + let skips = 0; + + // Arranco en -1 porque no quiero devolver lo que ya esta matcheado, en esta funcion puedo devolver undefined + + let currentASegmentStart = nodeA.index; + let currentBSegmentStart = nodeB.index; + + let indexA = nodeA.index - 1; + let indexB = nodeB.index - 1; + + assert(equals(nodeA, nodeB), () => "Misaligned matched"); + + mainLoop: while (true) { + // TODO-2 First iteration already has the nodes + const nextA = iterA.peek(indexA); + const nextB = iterB.peek(indexB); + + // If one of the iterators ends then there is no more search to do + if (!nextA || !nextB) { + if (segmentLength === 0) { + return; + } + + // BACKWARDS since end is excluvive we dont need to deal with reverting the false step, for backwards we need to handle that + + segments.push([ + currentASegmentStart - segmentLength, + currentBSegmentStart - segmentLength, + segmentLength, + ]); + break mainLoop; + } + + if (equals(nextA, nextB)) { + segmentLength++; + // BACKWARDS + indexA--; + indexB--; + continue; + } + + // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment + // BACKWARDs + + // nada me garantiza que halla algo para pushear porque arrancamos en -1 + if (segmentLength) { + segments.push([ + currentASegmentStart - segmentLength, + currentBSegmentStart - segmentLength, + segmentLength, + ]); + } + + segmentLength = 0; + + let skipsInLookaheadB = 0; + + // Verify the following n number of nodes ahead storing the ones equals to the wanted node + // + // A B C B C B B + // 0 1 2 3 4 5 6 + // + // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped + function scanForSameNodes( + iter: Iterator, + wantedNode: Node, + lastIndex: number, + ) { + const nodes: Node[] = []; + // BACKWARDS + for (let index = wantedNode.index - 1; index >= lastIndex; index--) { + const next = iter.peek(index); + + if (!next) { + continue; + } + + if (equals(wantedNode, next)) { + nodes.push(next); + } + } + return nodes; + } + + // We will skip N nodes on A before skipping 1 node on B, up to N skips in B. N = maxNodeSkips defined in the options + // For example: + // + // A: 1 2 w x y z 4 5 + // B: 1 2 3 4 5 + // + // - We match "1 2", match breaks when we react "3" on B + // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips + // - We skip on B, now it's "4", we start again from the original A position, which is "w" + // - After 4 skips we found a match + // - Final results it's: + // 1 2 _ _ _ _ 4 5 + // 1 2 _ 4 5 + + // BACKWARDS + const skipBUntil = Math.max(0, indexB - _context.options.maxNodeSkips); + // for (const newIndexB of range(indexB, skipBUntil)) { + for (let newIndexB = indexB; newIndexB >= skipBUntil; newIndexB--) { + // for (const newIndexB of range(indexB, skipBUntil, i => i - 1)) { + const newB = iterB.peek(newIndexB); + + if (!newB) { + break mainLoop; + } + + let skipsInLookaheadA = 0; + + // BACKWARDS + const skipAUntil = Math.max(0, indexA - _context.options.maxNodeSkips); + lookaheadA: for (let newIndexA = indexA; newIndexA >= skipAUntil; newIndexA--) { + // lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { + assert(newB); + + const newA = iterA.peek(newIndexA); + + if (!newA) { + break lookaheadA; + } + + // After the appropriate skips we need to test if we found a match + if (equals(newA, newB)) { + // Because we may had skipped on A and B we need to ensure that the nodes from the new match are the best candidate + // Since there may be same nodes ahead, for example, given: + // + // A: 1 2 3 4 + // B: 1 2 x 3 x 3 4 + // + // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from + // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 + // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 + // + // The second option is the less one since it hasless segments + const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); + const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); + + if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { + // RESUME + indexA = newIndexA; + indexB = newIndexB; + + currentASegmentStart = newIndexA + 1; + currentBSegmentStart = newIndexB + 1; + + skips += skipsInLookaheadA + skipsInLookaheadB; + continue mainLoop; + } + + // If we have more than one option we need to check all of them out before picking the best one + let bestCandidate = CandidateMatch.createEmpty(); + + for (const node of sameNodesAheadA) { + const newCandidate = exploreMatchBackward(node, newB); + + if (!newCandidate) { + // BACKWARDS + continue; + } + + if (newCandidate.isBetterThan(bestCandidate)) { + skipsInLookaheadA += node.index - newA.index; + bestCandidate = newCandidate; + } + } + + for (const node of sameNodesAheadB) { + const newCandidate = exploreMatchBackward(newA, node); + + if (!newCandidate) { + // BACKWARDS + continue; + } + + if (newCandidate.isBetterThan(bestCandidate)) { + skipsInLookaheadB += node.index - newB.index; + bestCandidate = newCandidate; + } + } + + if (bestCandidate.isEmpty()) { + // RESUME + indexA = newIndexA; + indexB = newIndexB; + + currentASegmentStart = newIndexA + 1; + currentBSegmentStart = newIndexB + 1; + + skips += skipsInLookaheadA + skipsInLookaheadB; + continue mainLoop; + } + + const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]); + + indexA = a.start; + indexB = b.start; + + currentASegmentStart = a.start; + currentBSegmentStart = b.start; + + skips += skipsInLookaheadA + skipsInLookaheadB; + + continue mainLoop; + } + + skipsInLookaheadA++; + } + + skipsInLookaheadB++; + } + + // Backwards only + if (segments.length === 0) { + return; + } + + break mainLoop; + } + + // BACKWARDS + segments.reverse(); + const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); + + // _context.matchesCache.add(Side.a, match, Direction.forward); + // _context.matchesCache.add(Side.b, match, Direction.forward); + + return match; +} From 90f366e1f2e971373e59924a90f1bb8d37780743 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 15 Apr 2024 19:41:12 -0300 Subject: [PATCH 71/79] Wip --- package.json | 5 ++--- src/debug.ts | 1 + tests/conformance/backwards-pass.test.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 939598e..0e2d320 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ }, "scripts": { "test": "vitest -c ./vitest.config.mjs", - "testNew": "vitest -c ./vitest.config.mjs additions-removals changes move sequence-matching trivia-diff", - "test-v2": "vitest -c ./vitest.config.mjs ./tests/v2", + "testNew": "vitest -c ./vitest.config.mjs additions-removals changes move sequence-matching trivia-diff backwards-pass", "build": "tsc --watch", "check": "npm run format && npm run lint", "format": "deno fmt", @@ -39,4 +38,4 @@ "vite": "4.4.2", "vitest": "1.4.0" } -} +} \ No newline at end of file diff --git a/src/debug.ts b/src/debug.ts index b5a1fb2..304e25b 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -47,6 +47,7 @@ export function assert( errorMessage?: () => string, ): asserts condition is NonNullable { if (!condition) { + debugger; fail(errorMessage?.()); } } diff --git a/tests/conformance/backwards-pass.test.ts b/tests/conformance/backwards-pass.test.ts index 43abc5d..2115f0e 100644 --- a/tests/conformance/backwards-pass.test.ts +++ b/tests/conformance/backwards-pass.test.ts @@ -5,7 +5,7 @@ import { test } from "../utils2"; // From "6 7" we add "5" which is next to it test({ - name: 'One node adjacent', + name: "One node adjacent", a: ` 5 6 7 `, @@ -23,7 +23,7 @@ test({ // From "6 7" we add "4 5" which are next to it test({ - name: 'Two nodes adjacent', + name: "Two nodes adjacent", a: ` 4 5 6 7 `, From 1b167c559cdc34d7500609c10b6ed8aaf8d8151f Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 15 Apr 2024 19:41:43 -0300 Subject: [PATCH 72/79] Working same function for backwards and forward exploration --- src/v2/exploreMatch.ts | 380 +++++++++++++++-------------------------- 1 file changed, 134 insertions(+), 246 deletions(-) diff --git a/src/v2/exploreMatch.ts b/src/v2/exploreMatch.ts index 70ac488..499ecb1 100644 --- a/src/v2/exploreMatch.ts +++ b/src/v2/exploreMatch.ts @@ -3,11 +3,11 @@ import { Iterator } from "./iterator"; import { _context } from "."; import { assert, fail } from "../debug"; import { Side } from "../shared/language"; -import { range, rangeEq } from "../utils"; +import { range, rangeEq, rangeEqBackwards } from "../utils"; import { CandidateMatch } from "./match"; import { trace } from "./tracing"; import { Segment } from "./types"; -import { equals, getIndexesFromSegment, getTextLengthFromSegments, skipFirstFromArray, skipLastFromArray } from "./utils"; +import { equals, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments, skipFirstFromArray, skipLastFromArray } from "./utils"; // This functions is `local` meaning that will move around only a tiny bit forward or backward. // This is in contrast to the `getBestMatch` fn that scan the whole list of nodes @@ -19,8 +19,8 @@ export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { // 3 4 5 6 7 // // Here we started from "5" and matched "5 6 7", but we can extend the match by going backwards. The same skipping logic applies for both directions - const forwardPass = exploreMatchForward(nodeA, nodeB); - const backwardPass = exploreMatchBackward(nodeA, nodeB); + const forwardPass = exploreMatchInDirection(nodeA, nodeB, true); + const backwardPass = exploreMatchInDirection(nodeA, nodeB, false); if (backwardPass) { trace("We can extend the match by going backward", backwardPass); @@ -57,220 +57,51 @@ export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { return forwardPass; } -function exploreMatchForward(nodeA: Node, nodeB: Node): CandidateMatch { +// This is the explanation of the explore match algorithm which is one of the key part of the whole tool. The explanation is +// divided in section and then we have the implementation bellow. There are two of them, they are 90% equal but i decided to +// duplicate them so that it's simpler to understand +// +// Algorithm: +// 1- Start from a known match +// 2- Advance to the next nodes on both side. This can be either forward or backwards +// 3- If there is a match, go back to 2 +// 4- If there is no match start the match recovery process, where we skip nodes on both sides until we recover the match or run out of skips +// 4a- Skip n times on A checking if we can recover +// If a match is found perform the match verification +// 4b- If no match is found skip 1 on B +// If a match is found set the state and jump to 2 +// 4c- Exit with current segments found +// +// Match verification +// Are there more nodes of the same in advance? +// If not, we are safe to resume, set the state and jump to 2 +// If there are nodes ahead we need to check all of them and pick the best +// With the best selected, set the sate and jump to 2 + +function exploreMatchInDirection(nodeA: Node, nodeB: Node, forward: isForward): isForward extends true ? CandidateMatch : CandidateMatch | undefined { assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); + assert(equals(nodeA, nodeB), () => "Misaligned matched"); - const segments: Segment[] = []; const { iterA, iterB } = _context; + const segments: Segment[] = []; let segmentLength = 0; let skips = 0; - let currentASegmentStart = nodeA.index; - let currentBSegmentStart = nodeB.index; - let indexA = nodeA.index; let indexB = nodeB.index; - assert(equals(nodeA, nodeB), () => "Misaligned matched"); - - mainLoop: while (true) { - // TODO-2 First iteration already has the nodes - const nextA = iterA.peek(indexA); - const nextB = iterB.peek(indexB); - - // If one of the iterators ends then there is no more search to do - if (!nextA || !nextB) { - assert(segmentLength > 0, () => "Segment length is 0"); - - segments.push([ - currentASegmentStart, - currentBSegmentStart, - segmentLength, - ]); - break mainLoop; - } - - if (equals(nextA, nextB)) { - segmentLength++; - indexA++; - indexB++; - continue; - } - - assert(segmentLength > 0, () => "Segment length is 0"); - - // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment - segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); - segmentLength = 0; - - let skipsInLookaheadB = 0; - - // Verify the following n number of nodes ahead storing the ones equals to the wanted node - // - // A B C B C B B - // 0 1 2 3 4 5 6 - // - // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped - function scanForSameNodes( - iter: Iterator, - wantedNode: Node, - lastIndex: number, - ) { - const nodes: Node[] = []; - for (const index of rangeEq(wantedNode.index + 1, lastIndex)) { - const next = iter.peek(index); - - if (!next) { - continue; - } - - if (equals(wantedNode, next)) { - nodes.push(next); - } - } - return nodes; - } - - // We will skip N nodes on A before skipping 1 node on B, up to N skips in B. N = maxNodeSkips defined in the options - // For example: - // - // A: 1 2 w x y z 4 5 - // B: 1 2 3 4 5 - // - // - We match "1 2", match breaks when we react "3" on B - // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips - // - We skip on B, now it's "4", we start again from the original A position, which is "w" - // - After 4 skips we found a match - // - Final results it's: - // 1 2 _ _ _ _ 4 5 - // 1 2 _ 4 5 - - const skipBUntil = Math.min(iterB.nodes.length, indexB + _context.options.maxNodeSkips); - for (const newIndexB of range(indexB, skipBUntil)) { - const newB = iterB.peek(newIndexB); - - if (!newB) { - break mainLoop; - } - - let skipsInLookaheadA = 0; - - const skipAUntil = Math.min(iterA.nodes.length, indexA + _context.options.maxNodeSkips); - lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { - assert(newB); - - const newA = iterA.peek(newIndexA); - - if (!newA) { - break lookaheadA; - } - - // After the appropriate skips we need to test if we found a match - if (equals(newA, newB)) { - // Because we may had skipped on A and B we need to ensure that the nodes from the new match are the best candidate - // Since there may be same nodes ahead, for example, given: - // - // A: 1 2 3 4 - // B: 1 2 x 3 x 3 4 - // - // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from - // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 - // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 - // - // The second option is the less one since it has less segments - const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); - const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); - - if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { - indexA = newIndexA; - indexB = newIndexB; - - currentASegmentStart = newIndexA; - currentBSegmentStart = newIndexB; - - skips += skipsInLookaheadA + skipsInLookaheadB; - continue mainLoop; - } - - // If we have more than one option we need to check all of them out before picking the best one - let bestCandidate = CandidateMatch.createEmpty(); - - for (const node of sameNodesAheadA) { - const newCandidate = exploreMatchForward(node, newB); - - if (!newCandidate) { - fail(); - } - - if (newCandidate.isBetterThan(bestCandidate)) { - skipsInLookaheadA += node.index - newA.index; - bestCandidate = newCandidate; - } - } - - for (const node of sameNodesAheadB) { - const newCandidate = exploreMatchForward(newA, node); - - if (!newCandidate) { - fail(); - } - - if (newCandidate.isBetterThan(bestCandidate)) { - skipsInLookaheadB += node.index - newB.index; - bestCandidate = newCandidate; - } - } - - const { a, b } = getIndexesFromSegment(bestCandidate.segments[0]); - - indexA = a.start; - indexB = b.start; - - currentASegmentStart = a.start; - currentBSegmentStart = b.start; - - skips += skipsInLookaheadA + skipsInLookaheadB; - - continue mainLoop; - } - - skipsInLookaheadA++; - } - - skipsInLookaheadB++; - } - - break mainLoop; + // This is the main difference between going forward and backwards. Since the main driver is going forward and going backwards + // just tries to extend the match, we can return undefined when going that direction. We know that at indexA and indexB we have the match + // lets skip that and try to extend it right away, if we fail to do so we just return `undefined` and move on + if (!forward) { + indexA--; + indexB--; } - const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); - - // _context.matchesCache.add(Side.a, match, Direction.forward); - // _context.matchesCache.add(Side.b, match, Direction.forward); - - return match; -} - -function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefined { - assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); - - const segments: Segment[] = []; - const { iterA, iterB } = _context; - - let segmentLength = 0; - let skips = 0; - - // Arranco en -1 porque no quiero devolver lo que ya esta matcheado, en esta funcion puedo devolver undefined - let currentASegmentStart = nodeA.index; let currentBSegmentStart = nodeB.index; - let indexA = nodeA.index - 1; - let indexB = nodeB.index - 1; - - assert(equals(nodeA, nodeB), () => "Misaligned matched"); - mainLoop: while (true) { // TODO-2 First iteration already has the nodes const nextA = iterA.peek(indexA); @@ -279,14 +110,30 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi // If one of the iterators ends then there is no more search to do if (!nextA || !nextB) { if (segmentLength === 0) { - return; + if (forward) { + fail("Segment length is 0"); + } else { + return undefined!; + } } - // BACKWARDS since end is excluvive we dont need to deal with reverting the false step, for backwards we need to handle that + // Given + // _ // Index A at 2 + // 1 2 3 4 5 + // _ // Index B at 3 + // x 1 2 3 4 5 + // + // When going forward we simply push to the segment the starting indexes, in this case we will get [2, 3, 3]. The last 3 is for the length + // But when going backwards we record where we started, then increase the segment and when we want to exit we need to calculate the starting point + // Following the above example, if we are at 3 and we go backwards, the starting indexes will be 2 and 3 respectively (same as going forward) + // the length will be 2, so + // Segment A start = 2 - 2 = 0 + // Segment B start = 3 - 2 = 1 + // Segment = [0, 1, 2] segments.push([ - currentASegmentStart - segmentLength, - currentBSegmentStart - segmentLength, + forward ? currentASegmentStart : currentASegmentStart - segmentLength, + forward ? currentBSegmentStart : currentBSegmentStart - segmentLength, segmentLength, ]); break mainLoop; @@ -294,28 +141,36 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi if (equals(nextA, nextB)) { segmentLength++; - // BACKWARDS - indexA--; - indexB--; + + if (forward) { + indexA++; + indexB++; + } else { + indexA--; + indexB--; + } + continue; } - // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment - // BACKWARDs - - // nada me garantiza que halla algo para pushear porque arrancamos en -1 - if (segmentLength) { - segments.push([ - currentASegmentStart - segmentLength, - currentBSegmentStart - segmentLength, - segmentLength, - ]); + if (forward) { + assert(segmentLength > 0, () => "Segment length is 0"); + segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); + } else { + if (segmentLength) { + // As mentioned above we need to calculate the starting point of the segment based on the length before pushing it + segments.push([ + currentASegmentStart - segmentLength, + currentBSegmentStart - segmentLength, + segmentLength, + ]); + } } + // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment segmentLength = 0; let skipsInLookaheadB = 0; - // Verify the following n number of nodes ahead storing the ones equals to the wanted node // // A B C B C B B @@ -329,7 +184,11 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi ) { const nodes: Node[] = []; // BACKWARDS - for (let index = wantedNode.index - 1; index >= lastIndex; index--) { + // FORWARD for (const index of rangeEq(wantedNode.index + 1, lastIndex)) { + // BACKWARDS for (let index = wantedNode.index - 1; index >= lastIndex; index--) { + const rangeIter = forward ? rangeEq(wantedNode.index + 1, lastIndex) : rangeEqBackwards(wantedNode.index - 1, lastIndex); + + for (const index of rangeIter) { const next = iter.peek(index); if (!next) { @@ -340,6 +199,7 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi nodes.push(next); } } + return nodes; } @@ -358,10 +218,20 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi // 1 2 _ 4 5 // BACKWARDS - const skipBUntil = Math.max(0, indexB - _context.options.maxNodeSkips); - // for (const newIndexB of range(indexB, skipBUntil)) { - for (let newIndexB = indexB; newIndexB >= skipBUntil; newIndexB--) { - // for (const newIndexB of range(indexB, skipBUntil, i => i - 1)) { + + // Given the following setup + // Iter with 12 nodes + // Current index at 8 + // Max skip nodes is 5 + // Going forward: 8 + 5 = 13, this is more that the iter nodes, so we take the minimum + // Consider now that the current index is 2 + // Going backwards: 2 - 5 = -3, we are also getting out of bound, so we pick the max between that and 0 + + const skipBUntil = forward ? getUpperBound(Side.b, indexB) : getLowerBound(indexB); + const rangeIter = forward ? range : rangeEqBackwards; + + const bRangeIter = rangeIter(indexB, skipBUntil); + for (const newIndexB of bRangeIter) { const newB = iterB.peek(newIndexB); if (!newB) { @@ -370,10 +240,9 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi let skipsInLookaheadA = 0; - // BACKWARDS - const skipAUntil = Math.max(0, indexA - _context.options.maxNodeSkips); - lookaheadA: for (let newIndexA = indexA; newIndexA >= skipAUntil; newIndexA--) { - // lookaheadA: for (const newIndexA of range(indexA, skipAUntil)) { + const skipAUntil = forward ? getUpperBound(Side.a, indexA) : getLowerBound(indexA); + const aRangeIter = rangeIter(indexA, skipAUntil); + lookaheadA: for (const newIndexA of aRangeIter) { assert(newB); const newA = iterA.peek(newIndexA); @@ -394,7 +263,7 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 // - // The second option is the less one since it hasless segments + // The second option is the less one since it has less segments const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); @@ -403,8 +272,8 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi indexA = newIndexA; indexB = newIndexB; - currentASegmentStart = newIndexA + 1; - currentBSegmentStart = newIndexB + 1; + currentASegmentStart = forward ? newIndexA : newIndexA + 1; + currentBSegmentStart = forward ? newIndexB : newIndexB + 1; skips += skipsInLookaheadA + skipsInLookaheadB; continue mainLoop; @@ -414,11 +283,14 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi let bestCandidate = CandidateMatch.createEmpty(); for (const node of sameNodesAheadA) { - const newCandidate = exploreMatchBackward(node, newB); + const newCandidate = exploreMatchInDirection(node, newB, forward); if (!newCandidate) { - // BACKWARDS - continue; + if (forward) { + fail(); + } else { + continue; + } } if (newCandidate.isBetterThan(bestCandidate)) { @@ -428,11 +300,14 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi } for (const node of sameNodesAheadB) { - const newCandidate = exploreMatchBackward(newA, node); + const newCandidate = exploreMatchInDirection(newA, node, forward); if (!newCandidate) { - // BACKWARDS - continue; + if (forward) { + fail(); + } else { + continue; + } } if (newCandidate.isBetterThan(bestCandidate)) { @@ -441,7 +316,11 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi } } - if (bestCandidate.isEmpty()) { + if (bestCandidate.isEmpty() && forward) { + fail(); + } + + if (bestCandidate.isEmpty() && !forward) { // RESUME indexA = newIndexA; indexB = newIndexB; @@ -473,19 +352,28 @@ function exploreMatchBackward(nodeA: Node, nodeB: Node): CandidateMatch | undefi } // Backwards only - if (segments.length === 0) { - return; + if (!forward && segments.length === 0) { + return undefined!; } break mainLoop; } // BACKWARDS - segments.reverse(); - const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); + if (!forward) { + segments.reverse(); + } - // _context.matchesCache.add(Side.a, match, Direction.forward); - // _context.matchesCache.add(Side.b, match, Direction.forward); + const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); return match; } + +function getLowerBound(index: number) { + return Math.max(0, index - _context.options.maxNodeSkips); +} + +function getUpperBound(side: Side, index: number) { + const iter = getIterFromSide(side); + return Math.min(iter.nodes.length, index + _context.options.maxNodeSkips); +} From fb5f79cb916b9f2fb0e51022f12f53cd1c9ec29c Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Mon, 15 Apr 2024 19:41:52 -0300 Subject: [PATCH 73/79] Fix style --- src/v2/match.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/match.ts b/src/v2/match.ts index 260a25e..6205807 100644 --- a/src/v2/match.ts +++ b/src/v2/match.ts @@ -31,7 +31,7 @@ export class CandidateMatch { } isEmpty() { - return this.segments.length === 0 + return this.segments.length === 0; } draw() { From 285ccd914c206a22ab314889f0ff3d5c0ceaeac7 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 16 Apr 2024 13:33:45 -0300 Subject: [PATCH 74/79] Cleanup pass 1 --- src/v2/exploreMatch.ts | 145 ++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 75 deletions(-) diff --git a/src/v2/exploreMatch.ts b/src/v2/exploreMatch.ts index 499ecb1..b27f8e1 100644 --- a/src/v2/exploreMatch.ts +++ b/src/v2/exploreMatch.ts @@ -9,17 +9,26 @@ import { trace } from "./tracing"; import { Segment } from "./types"; import { equals, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments, skipFirstFromArray, skipLastFromArray } from "./utils"; -// This functions is `local` meaning that will move around only a tiny bit forward or backward. -// This is in contrast to the `getBestMatch` fn that scan the whole list of nodes +/** + * Takes a match and iterates both forward and backwards trying to expand that match as long as possible. Skipping nodes if needed + * This function is considered `local` because it only visit nodes that are close by. + */ export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { - // We need both forward and backwards passes because our algorithm jumps around, which can leads us into a situation like this - // _____ - // .. 3 4 5 6 7 - // _____ - // 3 4 5 6 7 + // We need to go in both directions because the algorithm "jumps around", for example, if we have the nodes "1 2 3" + // and we only move to the next node by matching the previous one, when we visit "2" we can guarantee that "1" was matched + // but because out algorithm want the best absolute match when we see a "2" we need to check all possible "2" matches + // which could make us jump to a different "2" and start matching there. So we can find ourselves in this situation (simplified example): // - // Here we started from "5" and matched "5 6 7", but we can extend the match by going backwards. The same skipping logic applies for both directions + // _ + // A: 0 1 2 3 4 5 + // _ + // B: 0 1 2 3 4 5 + // + // Given that we start the match exploration at "2", we need to go forward to find the "3 4 5" and backwards to find the "0 1" + + // Going forward guarantees us a match, because this functions starts with one to begins with const forwardPass = exploreMatchInDirection(nodeA, nodeB, true); + // But there is no guarantee that we will find a match extension going backwards const backwardPass = exploreMatchInDirection(nodeA, nodeB, false); if (backwardPass) { @@ -91,9 +100,8 @@ function exploreMatchInDirection(nodeA: Node, nodeB: let indexA = nodeA.index; let indexB = nodeB.index; - // This is the main difference between going forward and backwards. Since the main driver is going forward and going backwards - // just tries to extend the match, we can return undefined when going that direction. We know that at indexA and indexB we have the match - // lets skip that and try to extend it right away, if we fail to do so we just return `undefined` and move on + // When going backwards we want to start the exploration with one step in already, because we know that at indexA & indexB there is a match + // we are only interested in extending that initial match, if that is not possible we return `undefined` if (!forward) { indexA--; indexB--; @@ -103,18 +111,14 @@ function exploreMatchInDirection(nodeA: Node, nodeB: let currentBSegmentStart = nodeB.index; mainLoop: while (true) { - // TODO-2 First iteration already has the nodes const nextA = iterA.peek(indexA); const nextB = iterB.peek(indexB); // If one of the iterators ends then there is no more search to do if (!nextA || !nextB) { if (segmentLength === 0) { - if (forward) { - fail("Segment length is 0"); - } else { - return undefined!; - } + assert(!forward, () => "Segment can'length t be 0 when exploring a match forward"); + return undefined!; } // Given @@ -140,6 +144,7 @@ function exploreMatchInDirection(nodeA: Node, nodeB: } if (equals(nextA, nextB)) { + // 1- Match continues segmentLength++; if (forward) { @@ -153,55 +158,23 @@ function exploreMatchInDirection(nodeA: Node, nodeB: continue; } + // 2- Match broke, store state and prepare for skipping + if (forward) { - assert(segmentLength > 0, () => "Segment length is 0"); + assert(segmentLength > 0, () => "Segment can'length t be 0 when exploring a match forward"); segments.push([currentASegmentStart, currentBSegmentStart, segmentLength]); - } else { - if (segmentLength) { - // As mentioned above we need to calculate the starting point of the segment based on the length before pushing it - segments.push([ - currentASegmentStart - segmentLength, - currentBSegmentStart - segmentLength, - segmentLength, - ]); - } + } else if (segmentLength !== 0) { + // As mentioned above we need to calculate the starting point of the segment based on the length before pushing it + segments.push([ + currentASegmentStart - segmentLength, + currentBSegmentStart - segmentLength, + segmentLength, + ]); } - // We found a discrepancy. Before try to skip nodes to recover the match we record the current segment segmentLength = 0; let skipsInLookaheadB = 0; - // Verify the following n number of nodes ahead storing the ones equals to the wanted node - // - // A B C B C B B - // 0 1 2 3 4 5 6 - // - // Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped - function scanForSameNodes( - iter: Iterator, - wantedNode: Node, - lastIndex: number, - ) { - const nodes: Node[] = []; - // BACKWARDS - // FORWARD for (const index of rangeEq(wantedNode.index + 1, lastIndex)) { - // BACKWARDS for (let index = wantedNode.index - 1; index >= lastIndex; index--) { - const rangeIter = forward ? rangeEq(wantedNode.index + 1, lastIndex) : rangeEqBackwards(wantedNode.index - 1, lastIndex); - - for (const index of rangeIter) { - const next = iter.peek(index); - - if (!next) { - continue; - } - - if (equals(wantedNode, next)) { - nodes.push(next); - } - } - - return nodes; - } // We will skip N nodes on A before skipping 1 node on B, up to N skips in B. N = maxNodeSkips defined in the options // For example: @@ -209,7 +182,7 @@ function exploreMatchInDirection(nodeA: Node, nodeB: // A: 1 2 w x y z 4 5 // B: 1 2 3 4 5 // - // - We match "1 2", match breaks when we react "3" on B + // - We match "1 2", match breaks when we visit "3" on B // - We skip on A trying to find "3" until A finishes or we run out of skips. In this case, we run out of skips // - We skip on B, now it's "4", we start again from the original A position, which is "w" // - After 4 skips we found a match @@ -217,19 +190,9 @@ function exploreMatchInDirection(nodeA: Node, nodeB: // 1 2 _ _ _ _ 4 5 // 1 2 _ 4 5 - // BACKWARDS - - // Given the following setup - // Iter with 12 nodes - // Current index at 8 - // Max skip nodes is 5 - // Going forward: 8 + 5 = 13, this is more that the iter nodes, so we take the minimum - // Consider now that the current index is 2 - // Going backwards: 2 - 5 = -3, we are also getting out of bound, so we pick the max between that and 0 - - const skipBUntil = forward ? getUpperBound(Side.b, indexB) : getLowerBound(indexB); const rangeIter = forward ? range : rangeEqBackwards; + const skipBUntil = forward ? getUpperBound(Side.b, indexB) : getLowerBound(indexB); const bRangeIter = rangeIter(indexB, skipBUntil); for (const newIndexB of bRangeIter) { const newB = iterB.peek(newIndexB); @@ -264,8 +227,8 @@ function exploreMatchInDirection(nodeA: Node, nodeB: // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 // // The second option is the less one since it has less segments - const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil); - const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil); + const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil, forward); + const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil, forward); if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { // RESUME @@ -369,11 +332,43 @@ function exploreMatchInDirection(nodeA: Node, nodeB: return match; } -function getLowerBound(index: number) { - return Math.max(0, index - _context.options.maxNodeSkips); +// Verify the following n number of nodes ahead storing the ones equals to the wanted node +// +// A B C B C B B +// 0 1 2 3 4 5 6 +// +// Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped +function scanForSameNodes( + iter: Iterator, + wantedNode: Node, + lastIndex: number, + forward: boolean, +) { + const nodes: Node[] = []; + const rangeIter = forward ? rangeEq(wantedNode.index + 1, lastIndex) : rangeEqBackwards(wantedNode.index - 1, lastIndex); + + for (const index of rangeIter) { + const next = iter.peek(index); + + if (!next) { + continue; + } + + if (equals(wantedNode, next)) { + nodes.push(next); + } + } + + return nodes; } +// When going forward we don't want to exceed the max number of nodes in a iter function getUpperBound(side: Side, index: number) { const iter = getIterFromSide(side); return Math.min(iter.nodes.length, index + _context.options.maxNodeSkips); } + +// When going backwards we don't want to go pass 0 +function getLowerBound(index: number) { + return Math.max(0, index - _context.options.maxNodeSkips); +} \ No newline at end of file From c2f3cea4ba875d95a091109a5af06e06e14cbd0f Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 16 Apr 2024 13:59:51 -0300 Subject: [PATCH 75/79] More cleanups --- src/v2/exploreMatch.ts | 71 +++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/src/v2/exploreMatch.ts b/src/v2/exploreMatch.ts index b27f8e1..59b6836 100644 --- a/src/v2/exploreMatch.ts +++ b/src/v2/exploreMatch.ts @@ -190,10 +190,7 @@ function exploreMatchInDirection(nodeA: Node, nodeB: // 1 2 _ _ _ _ 4 5 // 1 2 _ 4 5 - const rangeIter = forward ? range : rangeEqBackwards; - - const skipBUntil = forward ? getUpperBound(Side.b, indexB) : getLowerBound(indexB); - const bRangeIter = rangeIter(indexB, skipBUntil); + const [skipBUntil, bRangeIter] = getSkipIter(Side.b, indexB, forward); for (const newIndexB of bRangeIter) { const newB = iterB.peek(newIndexB); @@ -203,33 +200,36 @@ function exploreMatchInDirection(nodeA: Node, nodeB: let skipsInLookaheadA = 0; - const skipAUntil = forward ? getUpperBound(Side.a, indexA) : getLowerBound(indexA); - const aRangeIter = rangeIter(indexA, skipAUntil); + const [skipAUntil, aRangeIter] = getSkipIter(Side.a, indexA, forward); lookaheadA: for (const newIndexA of aRangeIter) { assert(newB); const newA = iterA.peek(newIndexA); if (!newA) { - break lookaheadA; + // We continue instead of breaking because maybe hit a matched node + continue lookaheadA; } - // After the appropriate skips we need to test if we found a match + // Found a match? if (equals(newA, newB)) { // Because we may had skipped on A and B we need to ensure that the nodes from the new match are the best candidate // Since there may be same nodes ahead, for example, given: - // + // ___ // A: 1 2 3 4 + // ___ // B: 1 2 x 3 x 3 4 // // If we already matched "1 2" and now we are trying to match "3" we will run into the following 2 cases since there are 2 "3" to choose from // 1 2 _ 3 _ _ 4 | Length 4 | Skips 3 | Segments 3 // 1 2 _ _ _ 3 4 | Length 4 | Skips 3 | Segments 2 // - // The second option is the less one since it has less segments + // The second option is the best one since it has less segments const sameNodesAheadA = scanForSameNodes(iterA, newA, skipAUntil, forward); const sameNodesAheadB = scanForSameNodes(iterB, newB, skipBUntil, forward); + // But, if there is no alternative we can safely resume + // 3- Resume after a match is found if (sameNodesAheadA.length === 0 && sameNodesAheadB.length === 0) { // RESUME indexA = newIndexA; @@ -249,11 +249,8 @@ function exploreMatchInDirection(nodeA: Node, nodeB: const newCandidate = exploreMatchInDirection(node, newB, forward); if (!newCandidate) { - if (forward) { - fail(); - } else { - continue; - } + assert(!forward); + continue; } if (newCandidate.isBetterThan(bestCandidate)) { @@ -266,11 +263,8 @@ function exploreMatchInDirection(nodeA: Node, nodeB: const newCandidate = exploreMatchInDirection(newA, node, forward); if (!newCandidate) { - if (forward) { - fail(); - } else { - continue; - } + assert(!forward); + continue; } if (newCandidate.isBetterThan(bestCandidate)) { @@ -313,17 +307,13 @@ function exploreMatchInDirection(nodeA: Node, nodeB: skipsInLookaheadB++; } - - // Backwards only - if (!forward && segments.length === 0) { - return undefined!; - } - break mainLoop; } - - // BACKWARDS if (!forward) { + if (segments.length === 0) { + return undefined!; + } + segments.reverse(); } @@ -332,12 +322,14 @@ function exploreMatchInDirection(nodeA: Node, nodeB: return match; } -// Verify the following n number of nodes ahead storing the ones equals to the wanted node -// -// A B C B C B B -// 0 1 2 3 4 5 6 -// -// Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped +/* + * Verify the following n number of nodes ahead storing the ones equals to the wanted node + * + * A B C B C B B + * 0 1 2 3 4 5 6 + * + * Looking for node "B" at index 3 you will get back nodes at indexes 3 and 6. Node at index 1 will be skipped + */ function scanForSameNodes( iter: Iterator, wantedNode: Node, @@ -362,6 +354,15 @@ function scanForSameNodes( return nodes; } +function getSkipIter(side: Side, index: number, forward: boolean) { + const iter = forward ? range : rangeEqBackwards; + + const skipUntil = forward ? getUpperBound(side, index) : getLowerBound(index); + const rangeIter = iter(index, skipUntil); + + return [skipUntil, rangeIter] as const; +} + // When going forward we don't want to exceed the max number of nodes in a iter function getUpperBound(side: Side, index: number) { const iter = getIterFromSide(side); @@ -371,4 +372,4 @@ function getUpperBound(side: Side, index: number) { // When going backwards we don't want to go pass 0 function getLowerBound(index: number) { return Math.max(0, index - _context.options.maxNodeSkips); -} \ No newline at end of file +} From 612ad3b1ac522add791d3a92f2585b9fb4549a09 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 16 Apr 2024 14:00:37 -0300 Subject: [PATCH 76/79] Remove old comment --- src/v2/exploreMatch.ts | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/v2/exploreMatch.ts b/src/v2/exploreMatch.ts index 59b6836..4ab14ef 100644 --- a/src/v2/exploreMatch.ts +++ b/src/v2/exploreMatch.ts @@ -66,27 +66,6 @@ export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { return forwardPass; } -// This is the explanation of the explore match algorithm which is one of the key part of the whole tool. The explanation is -// divided in section and then we have the implementation bellow. There are two of them, they are 90% equal but i decided to -// duplicate them so that it's simpler to understand -// -// Algorithm: -// 1- Start from a known match -// 2- Advance to the next nodes on both side. This can be either forward or backwards -// 3- If there is a match, go back to 2 -// 4- If there is no match start the match recovery process, where we skip nodes on both sides until we recover the match or run out of skips -// 4a- Skip n times on A checking if we can recover -// If a match is found perform the match verification -// 4b- If no match is found skip 1 on B -// If a match is found set the state and jump to 2 -// 4c- Exit with current segments found -// -// Match verification -// Are there more nodes of the same in advance? -// If not, we are safe to resume, set the state and jump to 2 -// If there are nodes ahead we need to check all of them and pick the best -// With the best selected, set the sate and jump to 2 - function exploreMatchInDirection(nodeA: Node, nodeB: Node, forward: isForward): isForward extends true ? CandidateMatch : CandidateMatch | undefined { assert(nodeA.side === Side.a && nodeB.side === Side.b, () => "Wrong sided nodes"); assert(equals(nodeA, nodeB), () => "Misaligned matched"); @@ -309,6 +288,7 @@ function exploreMatchInDirection(nodeA: Node, nodeB: } break mainLoop; } + if (!forward) { if (segments.length === 0) { return undefined!; From 16ae67a60c34121ce658ee1c72ad93922b9a51c0 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 16 Apr 2024 14:00:54 -0300 Subject: [PATCH 77/79] Updated comment --- src/v2/diff.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/v2/diff.ts b/src/v2/diff.ts index fbef915..6af3a2a 100644 --- a/src/v2/diff.ts +++ b/src/v2/diff.ts @@ -10,7 +10,8 @@ import { trace } from "./tracing"; import { exploreMatch } from "./exploreMatch"; /** - * Returns the longest possible match for the given node, this is including possible skips to improve the match length. + * Returns the longest possible match for the given node. No guarantees it's the best absolute match + * This function is considered `global` because it can visit any node in the iters */ export function getBestMatch(node: Node): CandidateMatch | undefined { const alreadyMatched = _context.verifyIfNodeWasMatched(node); From fececedbb2aa33b366c7b0a3125122b965b0a7ab Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 16 Apr 2024 14:12:41 -0300 Subject: [PATCH 78/79] added iters --- src/utils.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/utils.ts b/src/utils.ts index 813004b..52cde07 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -26,6 +26,24 @@ export function* rangeEq(start: number, end: number) { } } +export function* rangeBackwards(end: number, start: number) { + let i = end + 1; + + while (i > start + 1) { + i--; + yield i; + } +} + +export function* rangeEqBackwards(end: number, start: number) { + let i = end + 1; + + while (i >= start + 1) { + i--; + yield i; + } +} + export function oppositeSide(side: Side): Side { return side === Side.a ? Side.b : Side.a; } From 04866fbf8516ec9f7c9d9db9e5fc29f78aeb5676 Mon Sep 17 00:00:00 2001 From: Elian Cordoba Date: Tue, 16 Apr 2024 14:18:36 -0300 Subject: [PATCH 79/79] Match text length now is a computed property --- src/v2/change.ts | 3 ++- src/v2/core.ts | 5 +---- src/v2/exploreMatch.ts | 5 ++--- src/v2/match.ts | 37 +++++++++++++++++++++++++++++++++++-- src/v2/semanticAligment.ts | 4 +--- src/v2/utils.ts | 25 ------------------------- 6 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/v2/change.ts b/src/v2/change.ts index c498ffe..d213f31 100644 --- a/src/v2/change.ts +++ b/src/v2/change.ts @@ -2,7 +2,8 @@ import { Node } from "./node"; import { DiffType, TypeMasks } from "../types"; import { Segment } from "./types"; import { CandidateMatch } from "./match"; -import { getIndexesFromSegment, getTextLengthFromSegments } from "./utils"; +import { getIndexesFromSegment } from "./utils"; +import { getTextLengthFromSegments } from "./match"; import { assert } from "../debug"; import { _context } from "."; diff --git a/src/v2/core.ts b/src/v2/core.ts index a4965d2..fe13932 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -1,7 +1,7 @@ import colorFn from "kleur"; import { _context } from "."; import { getBestMatch, getBestMatchFromSubsequenceNodes } from "./diff"; -import { getIndexesFromSegment, getTextLengthFromSegments, matchContainsSingleNode, sort } from "./utils"; +import { getIndexesFromSegment, sort } from "./utils"; import { Iterator } from "./iterator"; import { DiffType } from "../types"; import { Change } from "./change"; @@ -145,10 +145,7 @@ function filterOutSingleSingleNodeFromMatch(match: CandidateMatch) { return; } else if (remainingSegments.length !== match.segments.length) { trace(`Match went from ${match.segments.length} segments to ${remainingSegments.length}`); - - // TODO-2: Update skips as well match.segments = remainingSegments; - match.textLength = getTextLengthFromSegments(remainingSegments); } return match; diff --git a/src/v2/exploreMatch.ts b/src/v2/exploreMatch.ts index 4ab14ef..b712e09 100644 --- a/src/v2/exploreMatch.ts +++ b/src/v2/exploreMatch.ts @@ -7,7 +7,7 @@ import { range, rangeEq, rangeEqBackwards } from "../utils"; import { CandidateMatch } from "./match"; import { trace } from "./tracing"; import { Segment } from "./types"; -import { equals, getIndexesFromSegment, getIterFromSide, getTextLengthFromSegments, skipFirstFromArray, skipLastFromArray } from "./utils"; +import { equals, getIndexesFromSegment, getIterFromSide, skipFirstFromArray, skipLastFromArray } from "./utils"; /** * Takes a match and iterates both forward and backwards trying to expand that match as long as possible. Skipping nodes if needed @@ -56,7 +56,6 @@ export function exploreMatch(nodeA: Node, nodeB: Node): CandidateMatch { ]; forwardPass.segments = [...head, middle, ...tail]; - forwardPass.textLength = getTextLengthFromSegments(forwardPass.segments); } else { // If the matches are not continuos, such as [1, 2] and [3, 4] there is no extra work to do forwardPass.segments = [...backwardPass.segments, ...forwardPass.segments]; @@ -297,7 +296,7 @@ function exploreMatchInDirection(nodeA: Node, nodeB: segments.reverse(); } - const match = new CandidateMatch(segments, getTextLengthFromSegments(segments), skips); + const match = new CandidateMatch(segments, skips); return match; } diff --git a/src/v2/match.ts b/src/v2/match.ts index 6205807..b6d40f8 100644 --- a/src/v2/match.ts +++ b/src/v2/match.ts @@ -1,17 +1,23 @@ import { _context } from "."; +import { assert } from "../debug"; +import { range } from "../utils"; import { Change } from "./change"; import { prettyPrintChanges } from "./debug"; import { Segment } from "./types"; +import { getIndexesFromSegment } from "./utils"; export class CandidateMatch { constructor( public segments: Segment[], - public textLength: number, public skips = 0, ) {} static createEmpty() { - return new CandidateMatch([], 0, 0); + return new CandidateMatch([], 0); + } + + get textLength() { + return getTextLengthFromSegments(this.segments); } isBetterThan(otherCandidate: CandidateMatch) { @@ -38,3 +44,30 @@ export class CandidateMatch { return prettyPrintChanges(_context.sourceA, _context.sourceB, [Change.createMove(this)]); } } +export function getTextLengthFromSegments(segments: Segment[]) { + if (segments.length === 0) { + return 0; + } + + let sum = 0; + + for (const segment of segments) { + const { a, b } = getIndexesFromSegment(segment); + + if (a.start !== -1) { + for (const i of range(a.start, a.end)) { + sum += _context.iterA.nodes[i].text.length; + } + } + + if (b.start !== -1) { + for (const i of range(b.start, b.end)) { + sum += _context.iterB.nodes[i].text.length; + } + } + } + + assert(sum > 0, () => "Segment length is 0"); + + return sum; +} diff --git a/src/v2/semanticAligment.ts b/src/v2/semanticAligment.ts index 7b36eae..2534a3a 100644 --- a/src/v2/semanticAligment.ts +++ b/src/v2/semanticAligment.ts @@ -3,7 +3,7 @@ import { DiffType } from "../types"; import { range, rangeEq } from "../utils"; import { Change, Move } from "./change"; import { Segment } from "./types"; -import { getIndexesFromSegment, getTextLengthFromSegments } from "./utils"; +import { getIndexesFromSegment } from "./utils"; export interface Offset { index: number; @@ -43,9 +43,7 @@ export function computeMoveAlignment(changes: Move[]): Move[] { continue; } else { if (unalignedSegments.length !== change.textLength) { - // TODO-2: Update skips as well change.segments = unalignedSegments; - change.textLength = getTextLengthFromSegments(unalignedSegments); } unalignedChanges.push(change); diff --git a/src/v2/utils.ts b/src/v2/utils.ts index fa07bea..d22d887 100644 --- a/src/v2/utils.ts +++ b/src/v2/utils.ts @@ -71,31 +71,6 @@ export function equals(nodeOne: Node, nodeTwo: Node): boolean { return nodeOne.kind === nodeTwo.kind && nodeOne.text === nodeTwo.text; } -// This function iterate for all the nodes (on both side if needed) of the match adding up the text length -export function getTextLengthFromSegments(segments: Segment[]) { - let sum = 0; - - for (const segment of segments) { - const { a, b } = getIndexesFromSegment(segment); - - if (a.start !== -1) { - for (const i of range(a.start, a.end)) { - sum += _context.iterA.nodes[i].text.length; - } - } - - if (b.start !== -1) { - for (const i of range(b.start, b.end)) { - sum += _context.iterB.nodes[i].text.length; - } - } - } - - assert(sum > 0, () => "Segment length is 0"); - - return sum; -} - export function getAllNodesFromSegment(iter: Iterator, side: Side, segment: Segment) { const nodes: Node[] = [];