mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2025-01-01 22:12:04 +03:00
Merge branch 'master' into visual-design-updates
This commit is contained in:
commit
e7341094b8
@ -3,7 +3,7 @@
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
@ -65,6 +65,7 @@
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"prettier-plugin-tailwindcss": "^0.2.4",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-check": "^3.0.1",
|
||||
"tailwindcss": "^3.1.5",
|
||||
|
@ -48,6 +48,7 @@ specifiers:
|
||||
posthog-js: ^1.46.1
|
||||
prettier: ^2.8.0
|
||||
prettier-plugin-svelte: ^2.8.1
|
||||
prettier-plugin-tailwindcss: ^0.2.4
|
||||
seti-icons: ^0.0.4
|
||||
svelte: ^3.55.1
|
||||
svelte-check: ^3.0.1
|
||||
@ -62,7 +63,7 @@ dependencies:
|
||||
'@codemirror/autocomplete': 6.4.2_yom6siklgbeshd7shgtg2sdiku
|
||||
'@codemirror/commands': 6.2.0
|
||||
'@codemirror/lang-angular': github.com/codemirror/lang-angular/ee6151b55668b2941c71b0d1db9f5a526ef29710
|
||||
'@codemirror/lang-css': github.com/codemirror/lang-css/9f5b41703dff289d94731c5caba72cf3b57fff43_nzpoxphwgc7witc3f5hdaoweju
|
||||
'@codemirror/lang-css': github.com/codemirror/lang-css/2cde46bf378ae36413e7fca5e24a2606f3cdf840_nzpoxphwgc7witc3f5hdaoweju
|
||||
'@codemirror/lang-html': github.com/codemirror/lang-html/0420487e1ac04bfd59129c243e3b7b802ffca30c
|
||||
'@codemirror/lang-java': github.com/codemirror/lang-java/834c534e7d689b0d88c15e3af55b0ee551d985d2
|
||||
'@codemirror/lang-javascript': 6.1.4
|
||||
@ -82,7 +83,7 @@ dependencies:
|
||||
'@lezer/lr': 1.3.3
|
||||
'@nextjournal/lang-clojure': 1.0.0
|
||||
'@replit/codemirror-lang-csharp': 6.1.0_dbd6aqsmfpystthdbxnrle4dwe
|
||||
'@replit/codemirror-lang-svelte': 6.0.0_zlhskyhbzw2pn2yfpaeuivpmfu
|
||||
'@replit/codemirror-lang-svelte': 6.0.0_yycrqtt34ynecrgehdjoffcaie
|
||||
'@square/svelte-store': 1.0.14
|
||||
'@tabler/icons-svelte': 2.6.0_svelte@3.55.1
|
||||
'@tauri-apps/api': 1.2.0
|
||||
@ -94,7 +95,7 @@ dependencies:
|
||||
posthog-js: 1.46.1
|
||||
seti-icons: 0.0.4
|
||||
svelte-french-toast: 1.0.3_svelte@3.55.1
|
||||
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/921afb3366b14ac43e3d8041a7def4b85d4d7192
|
||||
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/05a9bfd9edb9b5f4ab95412bb607691708b65a25
|
||||
|
||||
devDependencies:
|
||||
'@sveltejs/adapter-static': 1.0.0-next.50_l5ueyfihz3gpzzvvyo2ean5u3e
|
||||
@ -111,6 +112,7 @@ devDependencies:
|
||||
postcss-load-config: 4.0.1_postcss@8.4.21
|
||||
prettier: 2.8.4
|
||||
prettier-plugin-svelte: 2.9.0_jrsxveqmsx2uadbqiuq74wlc4u
|
||||
prettier-plugin-tailwindcss: 0.2.4_yjdjpc7im5hvpskij45owfsns4
|
||||
svelte: 3.55.1
|
||||
svelte-check: 3.0.3_gqx7lw3sljhsd4bstor5m2aa2u
|
||||
tailwindcss: 3.2.4_postcss@8.4.21
|
||||
@ -143,8 +145,8 @@ packages:
|
||||
'@lezer/common': 1.0.2
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-css/6.0.2_nzpoxphwgc7witc3f5hdaoweju:
|
||||
resolution: {integrity: sha512-4V4zmUOl2Glx0GWw0HiO1oGD4zvMlIQ3zx5hXOE6ipCjhohig2bhWRAasrZylH9pRNTcl1VMa59Lsl8lZWlTzw==}
|
||||
/@codemirror/lang-css/6.1.0_nzpoxphwgc7witc3f5hdaoweju:
|
||||
resolution: {integrity: sha512-GYn4TyMvQLrkrhdisFh8HCTDAjPY/9pzwN12hG9UdrTUxRUMicF+8GS24sFEYaleaG1KZClIFLCj0Rol/WO24w==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.4.2_yom6siklgbeshd7shgtg2sdiku
|
||||
'@codemirror/language': 6.6.0
|
||||
@ -159,7 +161,7 @@ packages:
|
||||
resolution: {integrity: sha512-bqCBASkteKySwtIbiV/WCtGnn/khLRbbiV5TE+d9S9eQJD7BA4c5dTRm2b3bVmSpilff5EYxvB4PQaZzM/7cNw==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.4.2_yom6siklgbeshd7shgtg2sdiku
|
||||
'@codemirror/lang-css': 6.0.2_nzpoxphwgc7witc3f5hdaoweju
|
||||
'@codemirror/lang-css': 6.1.0_nzpoxphwgc7witc3f5hdaoweju
|
||||
'@codemirror/lang-javascript': 6.1.4
|
||||
'@codemirror/language': 6.6.0
|
||||
'@codemirror/state': 6.2.0
|
||||
@ -608,7 +610,7 @@ packages:
|
||||
'@lezer/lr': 1.3.3
|
||||
dev: false
|
||||
|
||||
/@replit/codemirror-lang-svelte/6.0.0_zlhskyhbzw2pn2yfpaeuivpmfu:
|
||||
/@replit/codemirror-lang-svelte/6.0.0_yycrqtt34ynecrgehdjoffcaie:
|
||||
resolution: {integrity: sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==}
|
||||
peerDependencies:
|
||||
'@codemirror/autocomplete': ^6.0.0
|
||||
@ -624,7 +626,7 @@ packages:
|
||||
'@lezer/lr': ^1.0.0
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.4.2_yom6siklgbeshd7shgtg2sdiku
|
||||
'@codemirror/lang-css': github.com/codemirror/lang-css/9f5b41703dff289d94731c5caba72cf3b57fff43_nzpoxphwgc7witc3f5hdaoweju
|
||||
'@codemirror/lang-css': github.com/codemirror/lang-css/2cde46bf378ae36413e7fca5e24a2606f3cdf840_nzpoxphwgc7witc3f5hdaoweju
|
||||
'@codemirror/lang-html': github.com/codemirror/lang-html/0420487e1ac04bfd59129c243e3b7b802ffca30c
|
||||
'@codemirror/lang-javascript': 6.1.4
|
||||
'@codemirror/language': 6.6.0
|
||||
@ -2505,6 +2507,62 @@ packages:
|
||||
svelte: 3.55.1
|
||||
dev: true
|
||||
|
||||
/prettier-plugin-tailwindcss/0.2.4_yjdjpc7im5hvpskij45owfsns4:
|
||||
resolution: {integrity: sha512-wMyugRI2yD8gqmMpZSS8kTA0gGeKozX/R+w8iWE+yiCZL09zY0SvfiHfHabNhjGhzxlQ2S2VuTxPE3T72vppCQ==}
|
||||
engines: {node: '>=12.17.0'}
|
||||
peerDependencies:
|
||||
'@ianvs/prettier-plugin-sort-imports': '*'
|
||||
'@prettier/plugin-php': '*'
|
||||
'@prettier/plugin-pug': '*'
|
||||
'@shopify/prettier-plugin-liquid': '*'
|
||||
'@shufo/prettier-plugin-blade': '*'
|
||||
'@trivago/prettier-plugin-sort-imports': '*'
|
||||
prettier: '>=2.2.0'
|
||||
prettier-plugin-astro: '*'
|
||||
prettier-plugin-css-order: '*'
|
||||
prettier-plugin-import-sort: '*'
|
||||
prettier-plugin-jsdoc: '*'
|
||||
prettier-plugin-organize-attributes: '*'
|
||||
prettier-plugin-organize-imports: '*'
|
||||
prettier-plugin-style-order: '*'
|
||||
prettier-plugin-svelte: '*'
|
||||
prettier-plugin-twig-melody: '*'
|
||||
peerDependenciesMeta:
|
||||
'@ianvs/prettier-plugin-sort-imports':
|
||||
optional: true
|
||||
'@prettier/plugin-php':
|
||||
optional: true
|
||||
'@prettier/plugin-pug':
|
||||
optional: true
|
||||
'@shopify/prettier-plugin-liquid':
|
||||
optional: true
|
||||
'@shufo/prettier-plugin-blade':
|
||||
optional: true
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
optional: true
|
||||
prettier-plugin-astro:
|
||||
optional: true
|
||||
prettier-plugin-css-order:
|
||||
optional: true
|
||||
prettier-plugin-import-sort:
|
||||
optional: true
|
||||
prettier-plugin-jsdoc:
|
||||
optional: true
|
||||
prettier-plugin-organize-attributes:
|
||||
optional: true
|
||||
prettier-plugin-organize-imports:
|
||||
optional: true
|
||||
prettier-plugin-style-order:
|
||||
optional: true
|
||||
prettier-plugin-svelte:
|
||||
optional: true
|
||||
prettier-plugin-twig-melody:
|
||||
optional: true
|
||||
dependencies:
|
||||
prettier: 2.8.4
|
||||
prettier-plugin-svelte: 2.9.0_jrsxveqmsx2uadbqiuq74wlc4u
|
||||
dev: true
|
||||
|
||||
/prettier/2.8.4:
|
||||
resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@ -3192,11 +3250,11 @@ packages:
|
||||
'@lezer/highlight': 1.1.3
|
||||
dev: false
|
||||
|
||||
github.com/codemirror/lang-css/9f5b41703dff289d94731c5caba72cf3b57fff43_nzpoxphwgc7witc3f5hdaoweju:
|
||||
resolution: {tarball: https://codeload.github.com/codemirror/lang-css/tar.gz/9f5b41703dff289d94731c5caba72cf3b57fff43}
|
||||
id: github.com/codemirror/lang-css/9f5b41703dff289d94731c5caba72cf3b57fff43
|
||||
github.com/codemirror/lang-css/2cde46bf378ae36413e7fca5e24a2606f3cdf840_nzpoxphwgc7witc3f5hdaoweju:
|
||||
resolution: {tarball: https://codeload.github.com/codemirror/lang-css/tar.gz/2cde46bf378ae36413e7fca5e24a2606f3cdf840}
|
||||
id: github.com/codemirror/lang-css/2cde46bf378ae36413e7fca5e24a2606f3cdf840
|
||||
name: '@codemirror/lang-css'
|
||||
version: 6.0.2
|
||||
version: 6.1.0
|
||||
prepare: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
@ -3217,7 +3275,7 @@ packages:
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.4.2_yom6siklgbeshd7shgtg2sdiku
|
||||
'@codemirror/lang-css': 6.0.2_nzpoxphwgc7witc3f5hdaoweju
|
||||
'@codemirror/lang-css': 6.1.0_nzpoxphwgc7witc3f5hdaoweju
|
||||
'@codemirror/lang-javascript': 6.1.4
|
||||
'@codemirror/language': 6.6.0
|
||||
'@codemirror/state': 6.2.0
|
||||
@ -3321,8 +3379,8 @@ packages:
|
||||
'@lezer/lr': 1.3.3
|
||||
dev: false
|
||||
|
||||
github.com/tauri-apps/tauri-plugin-log/921afb3366b14ac43e3d8041a7def4b85d4d7192:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/921afb3366b14ac43e3d8041a7def4b85d4d7192}
|
||||
github.com/tauri-apps/tauri-plugin-log/05a9bfd9edb9b5f4ab95412bb607691708b65a25:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/05a9bfd9edb9b5f4ab95412bb607691708b65a25}
|
||||
name: tauri-plugin-log-api
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
354
src-tauri/Cargo.lock
generated
354
src-tauri/Cargo.lock
generated
@ -17,6 +17,17 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
|
||||
dependencies = [
|
||||
"getrandom 0.2.8",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
@ -47,6 +58,23 @@ version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.15.1"
|
||||
@ -130,6 +158,15 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitpacking"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
@ -253,6 +290,12 @@ dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "census"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fafee10a5dd1cffcb5cc560e0d0df8803d7355a2b12272e3557dee57314cb6e"
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
@ -451,6 +494,30 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"memoffset 0.8.0",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.14"
|
||||
@ -460,6 +527,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@ -614,6 +687,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.8"
|
||||
@ -635,6 +714,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "embed_plist"
|
||||
version = "1.2.2"
|
||||
@ -661,6 +746,37 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fail"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe5e43d0f78a42ad591453aedb1d7ae631ce7ee445c7643691055a9ed8d3b01c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastdivide"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25c7df09945d65ea8d70b3321547ed414bbc540aad5bac6883d021b970f35b04"
|
||||
|
||||
[[package]]
|
||||
name = "fastfield_codecs"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "374a3a53c1bd5fb31b10084229290eafb0a05f260ec90f1f726afffda4877a8a"
|
||||
dependencies = [
|
||||
"fastdivide",
|
||||
"itertools",
|
||||
"log",
|
||||
"ownedbytes",
|
||||
"tantivy-bitpacker",
|
||||
"tantivy-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.8.0"
|
||||
@ -686,7 +802,7 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"memoffset 0.6.5",
|
||||
"rustc_version 0.3.3",
|
||||
]
|
||||
|
||||
@ -742,6 +858,16 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@ -1011,6 +1137,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tantivy",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-log",
|
||||
@ -1202,6 +1329,9 @@ name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@ -1267,6 +1397,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "htmlescape"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.8"
|
||||
@ -1447,6 +1583,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1455,6 +1594,15 @@ version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
@ -1577,6 +1725,12 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "levenshtein_automata"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25"
|
||||
|
||||
[[package]]
|
||||
name = "libappindicator"
|
||||
version = "0.7.1"
|
||||
@ -1694,6 +1848,7 @@ checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"pin-utils",
|
||||
"scoped-tls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1701,6 +1856,21 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a8cbbb2831780bc3b9c15a41f5b49222ef756b6730a95f3decfdd15903eb5a3"
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@ -1766,6 +1936,16 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "measure_time"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
@ -1790,6 +1970,15 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
@ -1825,7 +2014,7 @@ dependencies = [
|
||||
"libc",
|
||||
"mach2",
|
||||
"memmap2",
|
||||
"memoffset",
|
||||
"memoffset 0.6.5",
|
||||
"minidump-common",
|
||||
"nix",
|
||||
"scroll",
|
||||
@ -1874,6 +2063,15 @@ dependencies = [
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "murmurhash32"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d736ff882f0e85fe9689fb23db229616c4c00aee2b3ac282f666d8f20eb25d4a"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.11"
|
||||
@ -2104,6 +2302,15 @@ version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||
|
||||
[[package]]
|
||||
name = "oneshot"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc22d22931513428ea6cc089e942d38600e3d00976eef8c86de6b8a3aadec6eb"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "3.2.0"
|
||||
@ -2175,6 +2382,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "ownedbytes"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e957eaa64a299f39755416e5b3128c505e9d63a91d0453771ad2ccd3907f8db"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.15.10"
|
||||
@ -2591,6 +2807,28 @@ dependencies = [
|
||||
"cty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
@ -2707,12 +2945,28 @@ dependencies = [
|
||||
"windows 0.37.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-stemmers"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.3.3"
|
||||
@ -3258,6 +3512,96 @@ dependencies = [
|
||||
"version-compare 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy"
|
||||
version = "0.19.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bb26a6b22c84d8be41d99a14016d6f04d30d8d31a2ea411a8ab553af5cc490d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
"base64 0.13.1",
|
||||
"bitpacking",
|
||||
"byteorder",
|
||||
"census",
|
||||
"crc32fast",
|
||||
"crossbeam-channel",
|
||||
"downcast-rs",
|
||||
"fail",
|
||||
"fastdivide",
|
||||
"fastfield_codecs",
|
||||
"fs2",
|
||||
"htmlescape",
|
||||
"itertools",
|
||||
"levenshtein_automata",
|
||||
"log",
|
||||
"lru",
|
||||
"lz4_flex",
|
||||
"measure_time",
|
||||
"memmap2",
|
||||
"murmurhash32",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"oneshot",
|
||||
"ownedbytes",
|
||||
"rayon",
|
||||
"regex",
|
||||
"rust-stemmers",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"stable_deref_trait",
|
||||
"tantivy-bitpacker",
|
||||
"tantivy-common",
|
||||
"tantivy-fst",
|
||||
"tantivy-query-grammar",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"time",
|
||||
"uuid 1.3.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-bitpacker"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e71a0c95b82d4292b097a09b989a6380d28c3a86800c841a2d03bae1fc8b9fa6"
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-common"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14fef4182bb60df9a4b92cd8ecab39ba2e50a05542934af17eef1f49660705cb"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ownedbytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-fst"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"regex-syntax",
|
||||
"utf8-ranges",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-query-grammar"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343e3ada4c1c480953f6960f8a21ce9c76611480ffdd4f4e230fdddce0fc5331"
|
||||
dependencies = [
|
||||
"combine",
|
||||
"once_cell",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.15.8"
|
||||
@ -3852,6 +4196,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-ranges"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.6"
|
||||
|
@ -35,6 +35,7 @@ reqwest = "0.11.14"
|
||||
md5 = "0.7.0"
|
||||
urlencoding = "2.1.2"
|
||||
thiserror = "1.0.38"
|
||||
tantivy = "0.19.2"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
@ -1,3 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use difference::{Changeset, Difference};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -10,6 +11,46 @@ pub enum Operation {
|
||||
Delete((u32, u32)),
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
pub fn apply(&self, text: &mut Vec<char>) -> Result<()> {
|
||||
match self {
|
||||
Operation::Insert((index, chunk)) => {
|
||||
if *index as usize > text.len() {
|
||||
Err(anyhow::anyhow!(
|
||||
"Index out of bounds, {} > {}",
|
||||
index,
|
||||
text.len()
|
||||
))
|
||||
} else if *index as usize == text.len() {
|
||||
text.extend(chunk.chars());
|
||||
Ok(())
|
||||
} else {
|
||||
text.splice(*index as usize..*index as usize, chunk.chars());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Operation::Delete((index, len)) => {
|
||||
if *index as usize > text.len() {
|
||||
Err(anyhow::anyhow!(
|
||||
"Index out of bounds, {} > {}",
|
||||
index,
|
||||
text.len()
|
||||
))
|
||||
} else if *index as usize + *len as usize > text.len() {
|
||||
Err(anyhow::anyhow!(
|
||||
"Index + length out of bounds, {} > {}",
|
||||
index + len,
|
||||
text.len()
|
||||
))
|
||||
} else {
|
||||
text.splice(*index as usize..(*index + *len) as usize, "".chars());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_delta_operations(initial_text: &str, final_text: &str) -> Vec<Operation> {
|
||||
if initial_text == final_text {
|
||||
return vec![];
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use super::{deltas, operations};
|
||||
use anyhow::Result;
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TextDocument {
|
||||
@ -8,30 +8,21 @@ pub struct TextDocument {
|
||||
deltas: Vec<deltas::Delta>,
|
||||
}
|
||||
|
||||
impl TextDocument {
|
||||
fn apply_deltas(doc: &mut Vec<char>, deltas: &Vec<deltas::Delta>) {
|
||||
if deltas.len() == 0 {
|
||||
return;
|
||||
}
|
||||
for event in deltas {
|
||||
for operation in event.operations.iter() {
|
||||
match operation {
|
||||
operations::Operation::Insert((index, chunk)) => {
|
||||
doc.splice(*index as usize..*index as usize, chunk.chars());
|
||||
}
|
||||
operations::Operation::Delete((index, len)) => {
|
||||
doc.drain(*index as usize..(*index + *len) as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn apply_deltas(doc: &mut Vec<char>, deltas: &Vec<deltas::Delta>) -> Result<()> {
|
||||
for delta in deltas {
|
||||
for operation in &delta.operations {
|
||||
operation.apply(doc)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl TextDocument {
|
||||
// creates a new text document from a deltas.
|
||||
pub fn from_deltas(deltas: Vec<deltas::Delta>) -> TextDocument {
|
||||
pub fn from_deltas(deltas: Vec<deltas::Delta>) -> Result<TextDocument> {
|
||||
let mut doc = vec![];
|
||||
Self::apply_deltas(&mut doc, &deltas);
|
||||
TextDocument { doc, deltas }
|
||||
apply_deltas(&mut doc, &deltas)?;
|
||||
Ok(TextDocument { doc, deltas })
|
||||
}
|
||||
|
||||
pub fn get_deltas(&self) -> Vec<deltas::Delta> {
|
||||
@ -39,18 +30,18 @@ impl TextDocument {
|
||||
}
|
||||
|
||||
// returns a text document where internal state is seeded with value, and deltas are applied.
|
||||
pub fn new(value: &str, deltas: Vec<deltas::Delta>) -> TextDocument {
|
||||
pub fn new(value: &str, deltas: Vec<deltas::Delta>) -> Result<TextDocument> {
|
||||
let mut all_deltas = vec![deltas::Delta {
|
||||
operations: operations::get_delta_operations("", value),
|
||||
timestamp_ms: 0,
|
||||
}];
|
||||
all_deltas.append(&mut deltas.clone());
|
||||
let mut doc = vec![];
|
||||
Self::apply_deltas(&mut doc, &all_deltas);
|
||||
TextDocument { doc, deltas }
|
||||
apply_deltas(&mut doc, &all_deltas)?;
|
||||
Ok(TextDocument { doc, deltas })
|
||||
}
|
||||
|
||||
pub fn update(&mut self, value: &str) -> bool {
|
||||
pub fn update(&mut self, value: &str) -> Result<bool> {
|
||||
let diffs = operations::get_delta_operations(&self.to_string(), value);
|
||||
let event = deltas::Delta {
|
||||
operations: diffs,
|
||||
@ -60,11 +51,11 @@ impl TextDocument {
|
||||
.as_millis(),
|
||||
};
|
||||
if event.operations.len() == 0 {
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
Self::apply_deltas(&mut self.doc, &vec![event.clone()]);
|
||||
apply_deltas(&mut self.doc, &vec![event.clone()])?;
|
||||
self.deltas.push(event);
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
|
@ -3,14 +3,18 @@ use crate::deltas::{operations::Operation, text_document::TextDocument, Delta};
|
||||
#[test]
|
||||
fn test_new() {
|
||||
let document = TextDocument::new("hello world", vec![]);
|
||||
assert_eq!(document.is_ok(), true);
|
||||
let document = document.unwrap();
|
||||
assert_eq!(document.to_string(), "hello world");
|
||||
assert_eq!(document.get_deltas().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update() {
|
||||
let mut document = TextDocument::new("hello world", vec![]);
|
||||
document.update("hello world!");
|
||||
let document = TextDocument::new("hello world", vec![]);
|
||||
assert_eq!(document.is_ok(), true);
|
||||
let mut document = document.unwrap();
|
||||
document.update("hello world!").unwrap();
|
||||
assert_eq!(document.to_string(), "hello world!");
|
||||
assert_eq!(document.get_deltas().len(), 1);
|
||||
assert_eq!(document.get_deltas()[0].operations.len(), 1);
|
||||
@ -22,8 +26,10 @@ fn test_update() {
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let mut document = TextDocument::from_deltas(vec![]);
|
||||
document.update("hello world!");
|
||||
let document = TextDocument::from_deltas(vec![]);
|
||||
assert_eq!(document.is_ok(), true);
|
||||
let mut document = document.unwrap();
|
||||
document.update("hello world!").unwrap();
|
||||
assert_eq!(document.to_string(), "hello world!");
|
||||
assert_eq!(document.get_deltas().len(), 1);
|
||||
assert_eq!(document.get_deltas()[0].operations.len(), 1);
|
||||
@ -52,14 +58,18 @@ fn test_from_deltas() {
|
||||
],
|
||||
},
|
||||
]);
|
||||
assert_eq!(document.is_ok(), true);
|
||||
let document = document.unwrap();
|
||||
assert_eq!(document.to_string(), "held!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_line() {
|
||||
let mut document = TextDocument::from_deltas(vec![]);
|
||||
let document = TextDocument::from_deltas(vec![]);
|
||||
assert_eq!(document.is_ok(), true);
|
||||
let mut document = document.unwrap();
|
||||
|
||||
document.update("hello");
|
||||
document.update("hello").unwrap();
|
||||
assert_eq!(document.to_string(), "hello");
|
||||
assert_eq!(document.get_deltas().len(), 1);
|
||||
assert_eq!(document.get_deltas()[0].operations.len(), 1);
|
||||
@ -68,7 +78,7 @@ fn test_complex_line() {
|
||||
Operation::Insert((0, "hello".to_string()))
|
||||
);
|
||||
|
||||
document.update("hello world");
|
||||
document.update("hello world").unwrap();
|
||||
assert_eq!(document.to_string(), "hello world");
|
||||
assert_eq!(document.get_deltas().len(), 2);
|
||||
assert_eq!(document.get_deltas()[1].operations.len(), 1);
|
||||
@ -77,7 +87,7 @@ fn test_complex_line() {
|
||||
Operation::Insert((5, " world".to_string()))
|
||||
);
|
||||
|
||||
document.update("held!");
|
||||
document.update("held!").unwrap();
|
||||
assert_eq!(document.to_string(), "held!");
|
||||
assert_eq!(document.get_deltas().len(), 3);
|
||||
assert_eq!(document.get_deltas()[2].operations.len(), 2);
|
||||
@ -93,9 +103,11 @@ fn test_complex_line() {
|
||||
|
||||
#[test]
|
||||
fn test_multiline_add() {
|
||||
let mut document = TextDocument::from_deltas(vec![]);
|
||||
let document = TextDocument::from_deltas(vec![]);
|
||||
assert_eq!(document.is_ok(), true);
|
||||
let mut document = document.unwrap();
|
||||
|
||||
document.update("first");
|
||||
document.update("first").unwrap();
|
||||
assert_eq!(document.to_string(), "first");
|
||||
assert_eq!(document.get_deltas().len(), 1);
|
||||
assert_eq!(document.get_deltas()[0].operations.len(), 1);
|
||||
@ -104,7 +116,7 @@ fn test_multiline_add() {
|
||||
Operation::Insert((0, "first".to_string()))
|
||||
);
|
||||
|
||||
document.update("first\ntwo");
|
||||
document.update("first\ntwo").unwrap();
|
||||
assert_eq!(document.to_string(), "first\ntwo");
|
||||
assert_eq!(document.get_deltas().len(), 2);
|
||||
assert_eq!(document.get_deltas()[1].operations.len(), 1);
|
||||
@ -113,7 +125,7 @@ fn test_multiline_add() {
|
||||
Operation::Insert((5, "\ntwo".to_string()))
|
||||
);
|
||||
|
||||
document.update("first line\nline two");
|
||||
document.update("first line\nline two").unwrap();
|
||||
assert_eq!(document.to_string(), "first line\nline two");
|
||||
assert_eq!(document.get_deltas().len(), 3);
|
||||
assert_eq!(document.get_deltas()[2].operations.len(), 2);
|
||||
@ -129,9 +141,11 @@ fn test_multiline_add() {
|
||||
|
||||
#[test]
|
||||
fn test_multiline_remove() {
|
||||
let mut document = TextDocument::from_deltas(vec![]);
|
||||
let document = TextDocument::from_deltas(vec![]);
|
||||
assert_eq!(document.is_ok(), true);
|
||||
let mut document = document.unwrap();
|
||||
|
||||
document.update("first line\nline two");
|
||||
document.update("first line\nline two").unwrap();
|
||||
assert_eq!(document.to_string(), "first line\nline two");
|
||||
assert_eq!(document.get_deltas().len(), 1);
|
||||
assert_eq!(document.get_deltas()[0].operations.len(), 1);
|
||||
@ -140,7 +154,7 @@ fn test_multiline_remove() {
|
||||
Operation::Insert((0, "first line\nline two".to_string()))
|
||||
);
|
||||
|
||||
document.update("first\ntwo");
|
||||
document.update("first\ntwo").unwrap();
|
||||
assert_eq!(document.to_string(), "first\ntwo");
|
||||
assert_eq!(document.get_deltas().len(), 2);
|
||||
assert_eq!(document.get_deltas()[1].operations.len(), 2);
|
||||
@ -153,7 +167,7 @@ fn test_multiline_remove() {
|
||||
Operation::Delete((6, 5))
|
||||
);
|
||||
|
||||
document.update("first");
|
||||
document.update("first").unwrap();
|
||||
assert_eq!(document.to_string(), "first");
|
||||
assert_eq!(document.get_deltas().len(), 3);
|
||||
assert_eq!(document.get_deltas()[2].operations.len(), 1);
|
||||
@ -162,7 +176,7 @@ fn test_multiline_remove() {
|
||||
Operation::Delete((5, 4))
|
||||
);
|
||||
|
||||
document.update("");
|
||||
document.update("").unwrap();
|
||||
assert_eq!(document.to_string(), "");
|
||||
assert_eq!(document.get_deltas().len(), 4);
|
||||
assert_eq!(document.get_deltas()[3].operations.len(), 1);
|
||||
|
@ -3,6 +3,7 @@ mod events;
|
||||
mod fs;
|
||||
mod projects;
|
||||
mod repositories;
|
||||
mod search;
|
||||
mod sessions;
|
||||
mod storage;
|
||||
mod users;
|
||||
@ -12,7 +13,11 @@ use anyhow::{Context, Result};
|
||||
use deltas::Delta;
|
||||
use log;
|
||||
use serde::{ser::SerializeMap, Serialize};
|
||||
use std::{collections::HashMap, sync::mpsc};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::Range,
|
||||
sync::{mpsc, Mutex},
|
||||
};
|
||||
use storage::Storage;
|
||||
use tauri::{generate_context, Manager};
|
||||
use tauri_plugin_log::{
|
||||
@ -55,6 +60,35 @@ impl From<anyhow::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
pub projects_storage: projects::Storage,
|
||||
pub users_storage: users::Storage,
|
||||
pub deltas_searcher: Mutex<search::Deltas>,
|
||||
pub watchers: Mutex<watchers::Watcher>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(resolver: tauri::PathResolver) -> Result<Self> {
|
||||
let local_data_dir = resolver.app_local_data_dir().unwrap();
|
||||
log::info!("Local data dir: {:?}", local_data_dir,);
|
||||
let storage = Storage::from_path_resolver(&resolver);
|
||||
let projects_storage = projects::Storage::new(storage.clone());
|
||||
let users_storage = users::Storage::new(storage.clone());
|
||||
let deltas_searcher = search::Deltas::at(local_data_dir)?;
|
||||
let watchers = watchers::Watcher::new(
|
||||
projects_storage.clone(),
|
||||
users_storage.clone(),
|
||||
deltas_searcher.clone(),
|
||||
);
|
||||
Ok(Self {
|
||||
projects_storage,
|
||||
users_storage,
|
||||
deltas_searcher: deltas_searcher.into(),
|
||||
watchers: watchers.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const IS_DEV: bool = cfg!(debug_assertions);
|
||||
|
||||
fn app_title() -> String {
|
||||
@ -104,18 +138,53 @@ fn proxy_image(handle: tauri::AppHandle, src: &str) -> Result<String> {
|
||||
Ok(build_asset_url(&save_to.display().to_string()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn search(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
query: &str,
|
||||
limit: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
timestamp_ms_gte: Option<u64>,
|
||||
timestamp_ms_lt: Option<u64>,
|
||||
) -> Result<Vec<search::SearchResult>, Error> {
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
let query = search::SearchQuery {
|
||||
project_id: project_id.to_string(),
|
||||
q: query.to_string(),
|
||||
limit: limit.unwrap_or(100),
|
||||
offset,
|
||||
range: Range {
|
||||
start: timestamp_ms_gte.unwrap_or(0),
|
||||
end: timestamp_ms_lt.unwrap_or(u64::MAX),
|
||||
},
|
||||
};
|
||||
|
||||
let deltas_lock = app_state
|
||||
.deltas_searcher
|
||||
.lock()
|
||||
.map_err(|poison_err| anyhow::anyhow!("Lock poisoned: {:?}", poison_err))?;
|
||||
let deltas = deltas_lock
|
||||
.search(&query)
|
||||
.with_context(|| format!("Failed to search for {:?}", query))?;
|
||||
|
||||
Ok(deltas)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_sessions(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
) -> Result<Vec<sessions::Session>, Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let projects_storage = projects::Storage::new(storage.clone());
|
||||
let users_storage = users::Storage::new(storage);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
let repo = repositories::Repository::open(&projects_storage, &users_storage, project_id)
|
||||
.with_context(|| format!("Failed to open repository for project {}", project_id))?;
|
||||
let repo = repositories::Repository::open(
|
||||
&app_state.projects_storage,
|
||||
&app_state.users_storage,
|
||||
project_id,
|
||||
)
|
||||
.with_context(|| format!("Failed to open repository for project {}", project_id))?;
|
||||
|
||||
let sessions = repo
|
||||
.sessions()
|
||||
@ -126,11 +195,10 @@ fn list_sessions(
|
||||
|
||||
#[tauri::command]
|
||||
fn get_user(handle: tauri::AppHandle) -> Result<Option<users::User>, Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let users_storage = users::Storage::new(storage);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
match users_storage
|
||||
match app_state
|
||||
.users_storage
|
||||
.get()
|
||||
.with_context(|| "Failed to get user".to_string())?
|
||||
{
|
||||
@ -156,11 +224,10 @@ fn get_user(handle: tauri::AppHandle) -> Result<Option<users::User>, Error> {
|
||||
|
||||
#[tauri::command]
|
||||
fn set_user(handle: tauri::AppHandle, user: users::User) -> Result<(), Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let users_storage = users::Storage::new(storage);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
users_storage
|
||||
app_state
|
||||
.users_storage
|
||||
.set(&user)
|
||||
.with_context(|| "Failed to set user".to_string())?;
|
||||
|
||||
@ -171,11 +238,10 @@ fn set_user(handle: tauri::AppHandle, user: users::User) -> Result<(), Error> {
|
||||
|
||||
#[tauri::command]
|
||||
fn delete_user(handle: tauri::AppHandle) -> Result<(), Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let users_storage = users::Storage::new(storage);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
users_storage
|
||||
app_state
|
||||
.users_storage
|
||||
.delete()
|
||||
.with_context(|| "Failed to delete user".to_string())?;
|
||||
|
||||
@ -189,11 +255,10 @@ fn update_project(
|
||||
handle: tauri::AppHandle,
|
||||
project: projects::UpdateRequest,
|
||||
) -> Result<projects::Project, Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let projects_storage = projects::Storage::new(storage);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
let project = projects_storage
|
||||
let project = app_state
|
||||
.projects_storage
|
||||
.update_project(&project)
|
||||
.with_context(|| format!("Failed to update project {}", project.id))?;
|
||||
|
||||
@ -202,18 +267,10 @@ fn update_project(
|
||||
|
||||
#[tauri::command]
|
||||
fn add_project(handle: tauri::AppHandle, path: &str) -> Result<projects::Project, Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let projects_storage = projects::Storage::new(storage.clone());
|
||||
let users_storage = users::Storage::new(storage);
|
||||
let watchers_collection = handle.state::<watchers::WatcherCollection>();
|
||||
let watchers = watchers::Watcher::new(
|
||||
&watchers_collection,
|
||||
projects_storage.clone(),
|
||||
users_storage.clone(),
|
||||
);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
for project in projects_storage
|
||||
for project in app_state
|
||||
.projects_storage
|
||||
.list_projects()
|
||||
.with_context(|| "Failed to list projects".to_string())?
|
||||
{
|
||||
@ -221,21 +278,23 @@ fn add_project(handle: tauri::AppHandle, path: &str) -> Result<projects::Project
|
||||
if !project.deleted {
|
||||
return Err(Error::ProjectAlreadyExists);
|
||||
} else {
|
||||
projects_storage.update_project(&projects::UpdateRequest {
|
||||
id: project.id.clone(),
|
||||
deleted: Some(false),
|
||||
..Default::default()
|
||||
})?;
|
||||
app_state
|
||||
.projects_storage
|
||||
.update_project(&projects::UpdateRequest {
|
||||
id: project.id.clone(),
|
||||
deleted: Some(false),
|
||||
..Default::default()
|
||||
})?;
|
||||
return Ok(project);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let project = projects::Project::from_path(path.to_string())?;
|
||||
projects_storage.add_project(&project)?;
|
||||
app_state.projects_storage.add_project(&project)?;
|
||||
|
||||
let (tx, rx): (mpsc::Sender<events::Event>, mpsc::Receiver<events::Event>) = mpsc::channel();
|
||||
watchers.watch(tx, &project)?;
|
||||
app_state.watchers.lock().unwrap().watch(tx, &project)?;
|
||||
watch_events(handle, rx);
|
||||
|
||||
Ok(project)
|
||||
@ -243,37 +302,28 @@ fn add_project(handle: tauri::AppHandle, path: &str) -> Result<projects::Project
|
||||
|
||||
#[tauri::command]
|
||||
fn list_projects(handle: tauri::AppHandle) -> Result<Vec<projects::Project>, Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let projects_storage = projects::Storage::new(storage);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
let projects = projects_storage.list_projects()?;
|
||||
let projects = app_state.projects_storage.list_projects()?;
|
||||
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn delete_project(handle: tauri::AppHandle, id: &str) -> Result<(), Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let projects_storage = projects::Storage::new(storage.clone());
|
||||
let watchers_collection = handle.state::<watchers::WatcherCollection>();
|
||||
let users_storage = users::Storage::new(storage);
|
||||
let watchers = watchers::Watcher::new(
|
||||
&watchers_collection,
|
||||
projects_storage.clone(),
|
||||
users_storage.clone(),
|
||||
);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
match projects_storage.get_project(id)? {
|
||||
match app_state.projects_storage.get_project(id)? {
|
||||
Some(project) => {
|
||||
watchers.unwatch(project)?;
|
||||
app_state.watchers.lock().unwrap().unwatch(project)?;
|
||||
|
||||
projects_storage.update_project(&projects::UpdateRequest {
|
||||
id: id.to_string(),
|
||||
deleted: Some(true),
|
||||
..Default::default()
|
||||
})?;
|
||||
app_state
|
||||
.projects_storage
|
||||
.update_project(&projects::UpdateRequest {
|
||||
id: id.to_string(),
|
||||
deleted: Some(true),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -288,12 +338,13 @@ fn list_session_files(
|
||||
session_id: &str,
|
||||
paths: Option<Vec<&str>>,
|
||||
) -> Result<HashMap<String, String>, Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let projects_storage = projects::Storage::new(storage.clone());
|
||||
let users_storage = users::Storage::new(storage);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
let repo = repositories::Repository::open(&projects_storage, &users_storage, project_id)?;
|
||||
let repo = repositories::Repository::open(
|
||||
&app_state.projects_storage,
|
||||
&app_state.users_storage,
|
||||
project_id,
|
||||
)?;
|
||||
|
||||
let files = repo.files(session_id, paths)?;
|
||||
|
||||
@ -306,12 +357,13 @@ fn list_deltas(
|
||||
project_id: &str,
|
||||
session_id: &str,
|
||||
) -> Result<HashMap<String, Vec<Delta>>, Error> {
|
||||
let path_resolver = handle.path_resolver();
|
||||
let storage = storage::Storage::from_path_resolver(&path_resolver);
|
||||
let projects_storage = projects::Storage::new(storage.clone());
|
||||
let users_storage = users::Storage::new(storage);
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
let repo = repositories::Repository::open(&projects_storage, &users_storage, project_id)?;
|
||||
let repo = repositories::Repository::open(
|
||||
&app_state.projects_storage,
|
||||
&app_state.users_storage,
|
||||
project_id,
|
||||
)?;
|
||||
|
||||
let deltas = repo.deltas(session_id)?;
|
||||
|
||||
@ -381,50 +433,15 @@ fn main() {
|
||||
#[cfg(debug_assertions)]
|
||||
window.open_devtools();
|
||||
|
||||
let resolver = app.path_resolver();
|
||||
log::info!(
|
||||
"Local data dir: {:?}",
|
||||
resolver.app_local_data_dir().unwrap()
|
||||
);
|
||||
let app_state: App =
|
||||
App::new(app.path_resolver()).expect("Failed to initialize app state");
|
||||
|
||||
let storage = Storage::from_path_resolver(&resolver);
|
||||
let projects_storage = projects::Storage::new(storage.clone());
|
||||
let users_storage = users::Storage::new(storage);
|
||||
let watcher_collection = watchers::WatcherCollection::default();
|
||||
let watchers = watchers::Watcher::new(
|
||||
&watcher_collection,
|
||||
projects_storage.clone(),
|
||||
users_storage.clone(),
|
||||
);
|
||||
app.manage(app_state);
|
||||
|
||||
users_storage
|
||||
.get()
|
||||
.and_then(|user| match user {
|
||||
Some(user) => {
|
||||
sentry::configure_scope(|scope| scope.set_user(Some(user.clone().into())));
|
||||
Ok(())
|
||||
}
|
||||
None => Ok(()),
|
||||
})
|
||||
.expect("Failed to set user");
|
||||
|
||||
let (tx, rx): (mpsc::Sender<events::Event>, mpsc::Receiver<events::Event>) =
|
||||
mpsc::channel();
|
||||
|
||||
match projects_storage.list_projects() {
|
||||
Ok(projects) => {
|
||||
for project in projects {
|
||||
watchers
|
||||
.watch(tx.clone(), &project)
|
||||
.with_context(|| format!("Failed to watch project: {}", project.id))?
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("Failed to list projects: {:#}", e),
|
||||
}
|
||||
|
||||
watch_events(app.handle(), rx);
|
||||
|
||||
app.manage(watcher_collection);
|
||||
let app_handle = app.handle();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
init(app_handle).expect("Failed to initialize app");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@ -462,7 +479,8 @@ fn main() {
|
||||
list_session_files,
|
||||
set_user,
|
||||
delete_user,
|
||||
get_user
|
||||
get_user,
|
||||
search
|
||||
]);
|
||||
|
||||
let tauri_context = generate_context!();
|
||||
@ -496,6 +514,50 @@ fn main() {
|
||||
);
|
||||
}
|
||||
|
||||
fn init(app_handle: tauri::AppHandle) -> Result<()> {
|
||||
let app_state = app_handle.state::<App>();
|
||||
|
||||
let user = app_state
|
||||
.users_storage
|
||||
.get()
|
||||
.with_context(|| "Failed to get user")?;
|
||||
|
||||
// setup senty
|
||||
if let Some(user) = user {
|
||||
sentry::configure_scope(|scope| scope.set_user(Some(user.clone().into())))
|
||||
}
|
||||
|
||||
// start watching projects
|
||||
let (tx, rx): (mpsc::Sender<events::Event>, mpsc::Receiver<events::Event>) = mpsc::channel();
|
||||
|
||||
let projects = app_state
|
||||
.projects_storage
|
||||
.list_projects()
|
||||
.with_context(|| "Failed to list projects")?;
|
||||
|
||||
for project in projects {
|
||||
app_state
|
||||
.watchers
|
||||
.lock()
|
||||
.unwrap()
|
||||
.watch(tx.clone(), &project)
|
||||
.with_context(|| format!("Failed to watch project: {}", project.id))?;
|
||||
|
||||
let repo = git2::Repository::open(&project.path)
|
||||
.with_context(|| format!("Failed to open git repository: {}", project.path))?;
|
||||
|
||||
app_state
|
||||
.deltas_searcher
|
||||
.lock()
|
||||
.unwrap()
|
||||
.reindex_project(&repo, &project)
|
||||
.with_context(|| format!("Failed to reindex project: {}", project.id))?;
|
||||
}
|
||||
watch_events(app_handle, rx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn watch_events(handle: tauri::AppHandle, rx: mpsc::Receiver<events::Event>) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Ok(event) = rx.recv() {
|
||||
|
346
src-tauri/src/search/deltas.rs
Normal file
346
src-tauri/src/search/deltas.rs
Normal file
@ -0,0 +1,346 @@
|
||||
use crate::{deltas, projects, sessions, storage};
|
||||
use anyhow::{Context, Result};
|
||||
use difference::Changeset;
|
||||
use serde::Serialize;
|
||||
use std::ops::Range;
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
time, vec,
|
||||
};
|
||||
use tantivy::{collector, directory::MmapDirectory, schema, IndexWriter};
|
||||
|
||||
const CURRENT_VERSION: u64 = 2; // should not decrease
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MetaStorage {
|
||||
storage: storage::Storage,
|
||||
}
|
||||
|
||||
impl MetaStorage {
|
||||
pub fn new(base_path: PathBuf) -> Self {
|
||||
Self {
|
||||
storage: storage::Storage::from_path(base_path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, project_id: &str, session_hash: &str) -> Result<Option<u64>> {
|
||||
let filepath = Path::new("indexes")
|
||||
.join("meta")
|
||||
.join(project_id)
|
||||
.join(session_hash);
|
||||
let meta = match self.storage.read(&filepath.to_str().unwrap())? {
|
||||
None => None,
|
||||
Some(meta) => meta.parse::<u64>().ok(),
|
||||
};
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
pub fn set(&self, project_id: &str, session_hash: &str, version: u64) -> Result<()> {
|
||||
let filepath = Path::new("indexes")
|
||||
.join("meta")
|
||||
.join(project_id)
|
||||
.join(session_hash);
|
||||
self.storage
|
||||
.write(&filepath.to_str().unwrap(), &version.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Deltas {
|
||||
meta_storage: MetaStorage,
|
||||
|
||||
index: tantivy::Index,
|
||||
reader: tantivy::IndexReader,
|
||||
writer: Arc<Mutex<tantivy::IndexWriter>>,
|
||||
}
|
||||
|
||||
impl Deltas {
|
||||
pub fn at(path: PathBuf) -> Result<Self> {
|
||||
let dir = path.join("indexes").join("deltas");
|
||||
fs::create_dir_all(&dir)?;
|
||||
|
||||
let mmap_dir = MmapDirectory::open(dir)?;
|
||||
let schema = build_schema();
|
||||
let index_settings = tantivy::IndexSettings {
|
||||
..Default::default()
|
||||
};
|
||||
let index = tantivy::IndexBuilder::new()
|
||||
.schema(schema)
|
||||
.settings(index_settings)
|
||||
.open_or_create(mmap_dir)?;
|
||||
|
||||
let reader = index.reader()?;
|
||||
let writer = index.writer(WRITE_BUFFER_SIZE)?;
|
||||
|
||||
Ok(Self {
|
||||
meta_storage: MetaStorage::new(path),
|
||||
reader,
|
||||
writer: Arc::new(Mutex::new(writer)),
|
||||
index,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>> {
|
||||
search(&self.index, &self.reader, query)
|
||||
}
|
||||
|
||||
pub fn reindex_project(
|
||||
&mut self,
|
||||
repo: &git2::Repository,
|
||||
project: &projects::Project,
|
||||
) -> Result<()> {
|
||||
let start = time::SystemTime::now();
|
||||
|
||||
let reference = repo.find_reference(&project.refname())?;
|
||||
let head = repo.find_commit(reference.target().unwrap())?;
|
||||
|
||||
// list all commits from gitbutler head to the first commit
|
||||
let mut walker = repo.revwalk()?;
|
||||
walker.push(head.id())?;
|
||||
walker.set_sorting(git2::Sort::TIME)?;
|
||||
|
||||
for oid in walker {
|
||||
let oid = oid?;
|
||||
let commit = repo
|
||||
.find_commit(oid)
|
||||
.with_context(|| format!("Could not find commit {}", oid.to_string()))?;
|
||||
let session_id = sessions::id_from_commit(repo, &commit)?;
|
||||
|
||||
let version = self
|
||||
.meta_storage
|
||||
.get(&project.id, &session_id)?
|
||||
.unwrap_or(0);
|
||||
|
||||
if version == CURRENT_VERSION {
|
||||
continue;
|
||||
}
|
||||
|
||||
let session = sessions::Session::from_commit(repo, &commit).with_context(|| {
|
||||
format!("Could not parse commit {} in project", oid.to_string())
|
||||
})?;
|
||||
self.index_session(repo, project, &session)
|
||||
.with_context(|| {
|
||||
format!("Could not index commit {} in project", oid.to_string())
|
||||
})?;
|
||||
}
|
||||
log::info!(
|
||||
"Reindexing project {} done, took {}ms",
|
||||
project.path,
|
||||
time::SystemTime::now().duration_since(start)?.as_millis()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn index_session(
|
||||
&mut self,
|
||||
repo: &git2::Repository,
|
||||
project: &projects::Project,
|
||||
session: &sessions::Session,
|
||||
) -> Result<()> {
|
||||
log::info!("Indexing session {} in {}", session.id, project.path);
|
||||
index(
|
||||
&self.index,
|
||||
&mut self.writer.lock().unwrap(),
|
||||
session,
|
||||
repo,
|
||||
project,
|
||||
)?;
|
||||
self.meta_storage
|
||||
.set(&project.id, &session.id, CURRENT_VERSION)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_schema() -> schema::Schema {
|
||||
let mut schema_builder = schema::Schema::builder();
|
||||
schema_builder.add_u64_field("version", schema::INDEXED | schema::FAST);
|
||||
schema_builder.add_text_field("project_id", schema::TEXT | schema::STORED | schema::FAST);
|
||||
schema_builder.add_text_field("session_id", schema::STORED);
|
||||
schema_builder.add_u64_field("index", schema::STORED);
|
||||
schema_builder.add_text_field("file_path", schema::TEXT | schema::STORED | schema::FAST);
|
||||
schema_builder.add_text_field("diff", schema::TEXT);
|
||||
schema_builder.add_bool_field("is_addition", schema::FAST);
|
||||
schema_builder.add_bool_field("is_deletion", schema::FAST);
|
||||
schema_builder.add_u64_field("timestamp_ms", schema::INDEXED | schema::FAST);
|
||||
schema_builder.build()
|
||||
}
|
||||
|
||||
const WRITE_BUFFER_SIZE: usize = 10_000_000; // 10MB
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SearchResult {
|
||||
pub project_id: String,
|
||||
pub session_id: String,
|
||||
pub file_path: String,
|
||||
pub index: u64,
|
||||
}
|
||||
|
||||
fn index(
|
||||
index: &tantivy::Index,
|
||||
writer: &mut IndexWriter,
|
||||
session: &sessions::Session,
|
||||
repo: &git2::Repository,
|
||||
project: &projects::Project,
|
||||
) -> Result<()> {
|
||||
let reference = repo.find_reference(&project.refname())?;
|
||||
let deltas = deltas::list(repo, project, &reference, &session.id)?;
|
||||
if deltas.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let files = sessions::list_files(
|
||||
repo,
|
||||
project,
|
||||
&reference,
|
||||
&session.id,
|
||||
Some(deltas.keys().map(|k| k.as_str()).collect()),
|
||||
)?;
|
||||
// index every file
|
||||
for (file_path, deltas) in deltas.into_iter() {
|
||||
// keep the state of the file after each delta operation
|
||||
// we need it to calculate diff for delete operations
|
||||
let mut file_text: Vec<char> = files
|
||||
.get(&file_path)
|
||||
.map(|f| f.as_str())
|
||||
.unwrap_or("")
|
||||
.chars()
|
||||
.collect();
|
||||
let mut prev_file_text = file_text.clone();
|
||||
// for every deltas for the file
|
||||
for (i, delta) in deltas.into_iter().enumerate() {
|
||||
let mut doc = tantivy::Document::default();
|
||||
doc.add_u64(
|
||||
index.schema().get_field("version").unwrap(),
|
||||
CURRENT_VERSION.try_into()?,
|
||||
);
|
||||
doc.add_u64(index.schema().get_field("index").unwrap(), i.try_into()?);
|
||||
doc.add_text(
|
||||
index.schema().get_field("session_id").unwrap(),
|
||||
session.id.clone(),
|
||||
);
|
||||
doc.add_text(
|
||||
index.schema().get_field("file_path").unwrap(),
|
||||
file_path.as_str(),
|
||||
);
|
||||
doc.add_text(
|
||||
index.schema().get_field("project_id").unwrap(),
|
||||
project.id.clone(),
|
||||
);
|
||||
doc.add_u64(
|
||||
index.schema().get_field("timestamp_ms").unwrap(),
|
||||
delta.timestamp_ms.try_into()?,
|
||||
);
|
||||
|
||||
// for every operation in the delta
|
||||
for operation in &delta.operations {
|
||||
// don't forget to apply the operation to the file_text
|
||||
if let Err(e) = operation.apply(&mut file_text) {
|
||||
log::error!("failed to apply operation: {:#}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut changeset = Changeset::new(
|
||||
&prev_file_text.iter().collect::<String>(),
|
||||
&file_text.iter().collect::<String>(),
|
||||
" ",
|
||||
);
|
||||
|
||||
changeset.diffs = changeset
|
||||
.diffs
|
||||
.into_iter()
|
||||
.filter(|d| match d {
|
||||
difference::Difference::Add(_) => true,
|
||||
difference::Difference::Rem(_) => true,
|
||||
difference::Difference::Same(_) => false,
|
||||
})
|
||||
.collect();
|
||||
|
||||
doc.add_text(index.schema().get_field("diff").unwrap(), changeset);
|
||||
|
||||
prev_file_text = file_text.clone();
|
||||
|
||||
writer.add_document(doc)?;
|
||||
}
|
||||
}
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SearchQuery {
|
||||
pub q: String,
|
||||
pub project_id: String,
|
||||
pub limit: usize,
|
||||
pub offset: Option<usize>,
|
||||
pub range: Range<u64>,
|
||||
}
|
||||
|
||||
pub fn search(
|
||||
index: &tantivy::Index,
|
||||
reader: &tantivy::IndexReader,
|
||||
q: &SearchQuery,
|
||||
) -> Result<Vec<SearchResult>> {
|
||||
let query = tantivy::query::QueryParser::for_index(
|
||||
index,
|
||||
vec![
|
||||
index.schema().get_field("diff").unwrap(),
|
||||
index.schema().get_field("file_path").unwrap(),
|
||||
],
|
||||
)
|
||||
.parse_query(
|
||||
format!(
|
||||
"version:\"{}\" AND project_id:\"{}\" AND timestamp_ms:[{} TO {}}} AND ({})",
|
||||
CURRENT_VERSION, q.project_id, q.range.start, q.range.end, q.q,
|
||||
)
|
||||
.as_str(),
|
||||
)?;
|
||||
|
||||
reader.reload()?;
|
||||
let searcher = reader.searcher();
|
||||
|
||||
let top_docs = searcher.search(
|
||||
&query,
|
||||
&collector::TopDocs::with_limit(q.limit)
|
||||
.and_offset(q.offset.unwrap_or(0))
|
||||
.order_by_u64_field(index.schema().get_field("timestamp_ms").unwrap()),
|
||||
)?;
|
||||
|
||||
let results = top_docs
|
||||
.iter()
|
||||
.map(|(_score, doc_address)| {
|
||||
let retrieved_doc = searcher.doc(*doc_address)?;
|
||||
let project_id = retrieved_doc
|
||||
.get_first(index.schema().get_field("project_id").unwrap())
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap();
|
||||
let file_path = retrieved_doc
|
||||
.get_first(index.schema().get_field("file_path").unwrap())
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap();
|
||||
let session_id = retrieved_doc
|
||||
.get_first(index.schema().get_field("session_id").unwrap())
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap();
|
||||
let index = retrieved_doc
|
||||
.get_first(index.schema().get_field("index").unwrap())
|
||||
.unwrap()
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
Ok(SearchResult {
|
||||
project_id: project_id.to_string(),
|
||||
file_path: file_path.to_string(),
|
||||
session_id: session_id.to_string(),
|
||||
index,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<SearchResult>>>()?;
|
||||
|
||||
Ok(results)
|
||||
}
|
244
src-tauri/src/search/deltas_test.rs
Normal file
244
src-tauri/src/search/deltas_test.rs
Normal file
@ -0,0 +1,244 @@
|
||||
use crate::{
|
||||
deltas::{self, Operation},
|
||||
projects, sessions,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use core::ops::Range;
|
||||
use std::path::Path;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn test_project() -> Result<(git2::Repository, projects::Project)> {
|
||||
let path = tempdir()?.path().to_str().unwrap().to_string();
|
||||
std::fs::create_dir_all(&path)?;
|
||||
let repo = git2::Repository::init(&path)?;
|
||||
let mut index = repo.index()?;
|
||||
let oid = index.write_tree()?;
|
||||
let sig = git2::Signature::now("test", "test@email.com").unwrap();
|
||||
let _commit = repo.commit(
|
||||
Some("HEAD"),
|
||||
&sig,
|
||||
&sig,
|
||||
"initial commit",
|
||||
&repo.find_tree(oid)?,
|
||||
&[],
|
||||
)?;
|
||||
let project = projects::Project::from_path(path)?;
|
||||
Ok((repo, project))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_by_timestamp() {
|
||||
let (repo, project) = test_project().unwrap();
|
||||
let index_path = tempdir().unwrap().path().to_str().unwrap().to_string();
|
||||
|
||||
let mut session = sessions::Session::from_head(&repo, &project).unwrap();
|
||||
deltas::write(
|
||||
&repo,
|
||||
&project,
|
||||
Path::new("test.txt"),
|
||||
&vec![
|
||||
deltas::Delta {
|
||||
operations: vec![Operation::Insert((0, "Hello".to_string()))],
|
||||
timestamp_ms: 0,
|
||||
},
|
||||
deltas::Delta {
|
||||
operations: vec![Operation::Insert((5, "World".to_string()))],
|
||||
timestamp_ms: 1,
|
||||
},
|
||||
deltas::Delta {
|
||||
operations: vec![Operation::Insert((5, " ".to_string()))],
|
||||
timestamp_ms: 2,
|
||||
},
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
session.flush(&repo, &None, &project).unwrap();
|
||||
|
||||
let mut searcher = super::Deltas::at(index_path.into()).unwrap();
|
||||
|
||||
let write_result = searcher.index_session(&repo, &project, &session);
|
||||
assert!(write_result.is_ok());
|
||||
|
||||
let search_result_from = searcher.search(&super::SearchQuery {
|
||||
project_id: project.id.clone(),
|
||||
q: "test.txt".to_string(),
|
||||
limit: 10,
|
||||
range: Range { start: 2, end: 10 },
|
||||
offset: None,
|
||||
});
|
||||
assert!(search_result_from.is_ok());
|
||||
let search_result_from = search_result_from.unwrap();
|
||||
assert_eq!(search_result_from.len(), 1);
|
||||
assert_eq!(search_result_from[0].index, 2);
|
||||
|
||||
let search_result_to = searcher.search(&super::SearchQuery {
|
||||
project_id: project.id.clone(),
|
||||
q: "hello world".to_string(),
|
||||
limit: 10,
|
||||
range: Range { start: 0, end: 1 },
|
||||
offset: None,
|
||||
});
|
||||
assert!(search_result_to.is_ok());
|
||||
let search_result_to = search_result_to.unwrap();
|
||||
assert_eq!(search_result_to.len(), 1);
|
||||
assert_eq!(search_result_to[0].index, 0);
|
||||
|
||||
let search_result_from_to = searcher.search(&super::SearchQuery {
|
||||
project_id: project.id.clone(),
|
||||
q: "hello world".to_string(),
|
||||
limit: 10,
|
||||
range: Range { start: 1, end: 2 },
|
||||
offset: None,
|
||||
});
|
||||
assert!(search_result_from_to.is_ok());
|
||||
let search_result_from_to = search_result_from_to.unwrap();
|
||||
assert_eq!(search_result_from_to.len(), 1);
|
||||
assert_eq!(search_result_from_to[0].index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sorted_by_timestamp() {
|
||||
let (repo, project) = test_project().unwrap();
|
||||
let index_path = tempdir().unwrap().path().to_str().unwrap().to_string();
|
||||
|
||||
let mut session = sessions::Session::from_head(&repo, &project).unwrap();
|
||||
deltas::write(
|
||||
&repo,
|
||||
&project,
|
||||
Path::new("test.txt"),
|
||||
&vec![
|
||||
deltas::Delta {
|
||||
operations: vec![Operation::Insert((0, "Hello".to_string()))],
|
||||
timestamp_ms: 0,
|
||||
},
|
||||
deltas::Delta {
|
||||
operations: vec![Operation::Insert((5, " World".to_string()))],
|
||||
timestamp_ms: 1,
|
||||
},
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
session.flush(&repo, &None, &project).unwrap();
|
||||
|
||||
let mut searcher = super::Deltas::at(index_path.into()).unwrap();
|
||||
|
||||
let write_result = searcher.index_session(&repo, &project, &session);
|
||||
assert!(write_result.is_ok());
|
||||
|
||||
let search_result = searcher.search(&super::SearchQuery {
|
||||
project_id: project.id,
|
||||
q: "hello world".to_string(),
|
||||
limit: 10,
|
||||
range: Range { start: 0, end: 10 },
|
||||
offset: None,
|
||||
});
|
||||
assert!(search_result.is_ok());
|
||||
let search_result = search_result.unwrap();
|
||||
println!("{:?}", search_result);
|
||||
assert_eq!(search_result.len(), 2);
|
||||
assert_eq!(search_result[0].index, 1);
|
||||
assert_eq!(search_result[1].index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let (repo, project) = test_project().unwrap();
|
||||
let index_path = tempdir().unwrap().path().to_str().unwrap().to_string();
|
||||
|
||||
let mut session = sessions::Session::from_head(&repo, &project).unwrap();
|
||||
deltas::write(
|
||||
&repo,
|
||||
&project,
|
||||
Path::new("test.txt"),
|
||||
&vec![
|
||||
deltas::Delta {
|
||||
operations: vec![Operation::Insert((0, "Hello".to_string()))],
|
||||
timestamp_ms: 0,
|
||||
},
|
||||
deltas::Delta {
|
||||
operations: vec![Operation::Insert((5, " World".to_string()))],
|
||||
timestamp_ms: 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
session.flush(&repo, &None, &project).unwrap();
|
||||
|
||||
let mut searcher = super::Deltas::at(index_path.into()).unwrap();
|
||||
|
||||
let write_result = searcher.index_session(&repo, &project, &session);
|
||||
assert!(write_result.is_ok());
|
||||
|
||||
let search_result1 = searcher.search(&super::SearchQuery {
|
||||
project_id: project.id.clone(),
|
||||
q: "hello".to_string(),
|
||||
limit: 10,
|
||||
offset: None,
|
||||
range: Range { start: 0, end: 10 },
|
||||
});
|
||||
println!("{:?}", search_result1);
|
||||
assert!(search_result1.is_ok());
|
||||
let search_result1 = search_result1.unwrap();
|
||||
assert_eq!(search_result1.len(), 1);
|
||||
assert_eq!(search_result1[0].session_id, session.id);
|
||||
assert_eq!(search_result1[0].project_id, project.id);
|
||||
assert_eq!(search_result1[0].file_path, "test.txt");
|
||||
assert_eq!(search_result1[0].index, 0);
|
||||
|
||||
let search_result2 = searcher.search(&super::SearchQuery {
|
||||
project_id: project.id.clone(),
|
||||
q: "world".to_string(),
|
||||
limit: 10,
|
||||
offset: None,
|
||||
range: Range { start: 0, end: 10 },
|
||||
});
|
||||
assert!(search_result2.is_ok());
|
||||
let search_result2 = search_result2.unwrap();
|
||||
assert_eq!(search_result2.len(), 1);
|
||||
assert_eq!(search_result2[0].session_id, session.id);
|
||||
assert_eq!(search_result2[0].project_id, project.id);
|
||||
assert_eq!(search_result2[0].file_path, "test.txt");
|
||||
assert_eq!(search_result2[0].index, 1);
|
||||
|
||||
let search_result3 = searcher.search(&super::SearchQuery {
|
||||
project_id: project.id.clone(),
|
||||
q: "hello world".to_string(),
|
||||
limit: 10,
|
||||
offset: None,
|
||||
range: Range { start: 0, end: 10 },
|
||||
});
|
||||
assert!(search_result3.is_ok());
|
||||
let search_result3 = search_result3.unwrap();
|
||||
assert_eq!(search_result3.len(), 2);
|
||||
assert_eq!(search_result3[0].project_id, project.id);
|
||||
assert_eq!(search_result3[0].session_id, session.id);
|
||||
assert_eq!(search_result3[0].file_path, "test.txt");
|
||||
assert_eq!(search_result3[1].session_id, session.id);
|
||||
assert_eq!(search_result3[1].project_id, project.id);
|
||||
assert_eq!(search_result3[1].file_path, "test.txt");
|
||||
|
||||
let search_by_filename_result = searcher.search(&super::SearchQuery {
|
||||
project_id: project.id.clone(),
|
||||
q: "test.txt".to_string(),
|
||||
limit: 10,
|
||||
offset: None,
|
||||
range: Range { start: 0, end: 10 },
|
||||
});
|
||||
assert!(search_by_filename_result.is_ok());
|
||||
let search_by_filename_result = search_by_filename_result.unwrap();
|
||||
assert_eq!(search_by_filename_result.len(), 2);
|
||||
assert_eq!(search_by_filename_result[0].session_id, session.id);
|
||||
assert_eq!(search_by_filename_result[0].project_id, project.id);
|
||||
assert_eq!(search_by_filename_result[0].file_path, "test.txt");
|
||||
|
||||
let not_found_result = searcher.search(&super::SearchQuery {
|
||||
project_id: "not found".to_string(),
|
||||
q: "test.txt".to_string(),
|
||||
limit: 10,
|
||||
offset: None,
|
||||
range: Range { start: 0, end: 10 },
|
||||
});
|
||||
assert!(not_found_result.is_ok());
|
||||
let not_found_result = not_found_result.unwrap();
|
||||
assert_eq!(not_found_result.len(), 0);
|
||||
}
|
6
src-tauri/src/search/mod.rs
Normal file
6
src-tauri/src/search/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
mod deltas;
|
||||
|
||||
pub use deltas::{Deltas, SearchQuery, SearchResult};
|
||||
|
||||
#[cfg(test)]
|
||||
mod deltas_test;
|
@ -1,7 +1,7 @@
|
||||
mod activity;
|
||||
mod sessions;
|
||||
|
||||
pub use sessions::{get, list, list_files, Session};
|
||||
pub use sessions::{get, list, list_files, Session, id_from_commit};
|
||||
|
||||
#[cfg(test)]
|
||||
mod activity_tests;
|
||||
|
@ -362,18 +362,14 @@ fn is_current_session_id(project: &projects::Project, session_id: &str) -> Resul
|
||||
return Ok(current_id == session_id);
|
||||
}
|
||||
|
||||
fn is_commit_session_id(
|
||||
repo: &git2::Repository,
|
||||
commit: &git2::Commit,
|
||||
session_id: &str,
|
||||
) -> Result<bool> {
|
||||
pub fn id_from_commit(repo: &git2::Repository, commit: &git2::Commit) -> Result<String> {
|
||||
let tree = commit.tree().unwrap();
|
||||
let session_id_path = Path::new("session/meta/id");
|
||||
if !tree.get_path(session_id_path).is_ok() {
|
||||
return Ok(false);
|
||||
return Err(anyhow!("commit does not have a session id"));
|
||||
}
|
||||
let id = read_as_string(repo, &tree, session_id_path)?;
|
||||
return Ok(id == session_id);
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
@ -393,7 +389,7 @@ pub fn get(
|
||||
|
||||
for commit_id in walker {
|
||||
let commit = repo.find_commit(commit_id?)?;
|
||||
if is_commit_session_id(repo, &commit, id)? {
|
||||
if id_from_commit(repo, &commit)? == id {
|
||||
return Ok(Some(Session::from_commit(repo, &commit)?));
|
||||
}
|
||||
}
|
||||
@ -471,7 +467,7 @@ pub fn list_files(
|
||||
let mut previous_session_commit = None;
|
||||
for commit_id in walker {
|
||||
let commit = repo.find_commit(commit_id?)?;
|
||||
if is_commit_session_id(repo, &commit, session_id)? {
|
||||
if id_from_commit(repo, &commit)? == session_id {
|
||||
session_commit = Some(commit);
|
||||
break;
|
||||
}
|
||||
|
@ -265,6 +265,7 @@ fn test_list_files_from_first_presistent_session() {
|
||||
let (repo, project) = test_project().unwrap();
|
||||
|
||||
let file_path = Path::new(&project.path).join("test.txt");
|
||||
|
||||
std::fs::write(file_path.clone(), "zero").unwrap();
|
||||
|
||||
let first = super::sessions::Session::from_head(&repo, &project);
|
||||
|
@ -1,5 +1,8 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::{fs, path::PathBuf};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tauri::PathResolver;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
@ -8,7 +11,6 @@ pub struct Storage {
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
#[cfg(test)]
|
||||
pub fn from_path(path: PathBuf) -> Self {
|
||||
Storage {
|
||||
local_data_dir: path,
|
||||
@ -21,7 +23,7 @@ impl Storage {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, path: &str) -> Result<Option<String>> {
|
||||
pub fn read<P: AsRef<Path>>(&self, path: P) -> Result<Option<String>> {
|
||||
let file_path = self.local_data_dir.join(path);
|
||||
if !file_path.exists() {
|
||||
return Ok(None);
|
||||
|
@ -4,25 +4,24 @@ use crate::{events, sessions};
|
||||
use anyhow::{Context, Result};
|
||||
use git2;
|
||||
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc;
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WatcherCollection(Mutex<HashMap<String, RecommendedWatcher>>);
|
||||
|
||||
pub struct DeltaWatchers<'a> {
|
||||
watchers: &'a WatcherCollection,
|
||||
pub struct DeltaWatchers {
|
||||
watchers: HashMap<String, RecommendedWatcher>,
|
||||
}
|
||||
|
||||
impl<'a> DeltaWatchers<'a> {
|
||||
pub fn new(watchers: &'a WatcherCollection) -> Self {
|
||||
Self { watchers }
|
||||
impl DeltaWatchers {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
watchers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn watch(
|
||||
&self,
|
||||
&mut self,
|
||||
sender: mpsc::Sender<events::Event>,
|
||||
project: projects::Project,
|
||||
) -> Result<()> {
|
||||
@ -34,11 +33,7 @@ impl<'a> DeltaWatchers<'a> {
|
||||
|
||||
watcher.watch(project_path, RecursiveMode::Recursive)?;
|
||||
|
||||
self.watchers
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(project.path.clone(), watcher);
|
||||
self.watchers.insert(project.path.clone(), watcher);
|
||||
|
||||
let repo = git2::Repository::open(project_path)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
@ -92,9 +87,8 @@ impl<'a> DeltaWatchers<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unwatch(&self, project: projects::Project) -> Result<()> {
|
||||
let mut watchers = self.watchers.0.lock().unwrap();
|
||||
if let Some(mut watcher) = watchers.remove(&project.path) {
|
||||
pub fn unwatch(&mut self, project: projects::Project) -> Result<()> {
|
||||
if let Some(mut watcher) = self.watchers.remove(&project.path) {
|
||||
watcher.unwatch(Path::new(&project.path))?;
|
||||
}
|
||||
Ok(())
|
||||
@ -150,13 +144,13 @@ fn register_file_change(
|
||||
|
||||
// depending on the above, we can create TextDocument suitable for calculating deltas
|
||||
let mut text_doc = match (latest_contents, deltas) {
|
||||
(Some(latest_contents), Some(deltas)) => TextDocument::new(&latest_contents, deltas),
|
||||
(Some(latest_contents), None) => TextDocument::new(&latest_contents, vec![]),
|
||||
(None, Some(deltas)) => TextDocument::from_deltas(deltas),
|
||||
(None, None) => TextDocument::from_deltas(vec![]),
|
||||
(Some(latest_contents), Some(deltas)) => TextDocument::new(&latest_contents, deltas)?,
|
||||
(Some(latest_contents), None) => TextDocument::new(&latest_contents, vec![])?,
|
||||
(None, Some(deltas)) => TextDocument::from_deltas(deltas)?,
|
||||
(None, None) => TextDocument::from_deltas(vec![])?,
|
||||
};
|
||||
|
||||
if !text_doc.update(&file_contents) {
|
||||
if !text_doc.update(&file_contents)? {
|
||||
return Ok(None);
|
||||
} else {
|
||||
// if the file was modified, save the deltas
|
||||
|
@ -1,43 +1,43 @@
|
||||
mod delta;
|
||||
mod git;
|
||||
mod session;
|
||||
|
||||
pub use self::delta::WatcherCollection;
|
||||
use crate::{events, projects, users};
|
||||
use crate::{events, projects, search, users};
|
||||
use anyhow::Result;
|
||||
use std::sync::mpsc;
|
||||
|
||||
pub struct Watcher<'a> {
|
||||
git_watcher: git::GitWatcher,
|
||||
delta_watcher: delta::DeltaWatchers<'a>,
|
||||
pub struct Watcher {
|
||||
session_watcher: session::SessionWatcher,
|
||||
delta_watcher: delta::DeltaWatchers,
|
||||
}
|
||||
|
||||
impl<'a> Watcher<'a> {
|
||||
impl Watcher {
|
||||
pub fn new(
|
||||
watchers: &'a delta::WatcherCollection,
|
||||
projects_storage: projects::Storage,
|
||||
users_storage: users::Storage,
|
||||
deltas_searcher: search::Deltas,
|
||||
) -> Self {
|
||||
let git_watcher = git::GitWatcher::new(projects_storage, users_storage);
|
||||
let delta_watcher = delta::DeltaWatchers::new(watchers);
|
||||
let session_watcher = session::SessionWatcher::new(projects_storage, users_storage, deltas_searcher);
|
||||
let delta_watcher = delta::DeltaWatchers::new();
|
||||
Self {
|
||||
git_watcher,
|
||||
session_watcher,
|
||||
delta_watcher,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn watch(
|
||||
&self,
|
||||
&mut self,
|
||||
sender: mpsc::Sender<events::Event>,
|
||||
project: &projects::Project,
|
||||
) -> Result<()> {
|
||||
self.delta_watcher.watch(sender.clone(), project.clone())?;
|
||||
self.git_watcher.watch(sender.clone(), project.clone())?;
|
||||
self.session_watcher
|
||||
.watch(sender.clone(), project.clone())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unwatch(&self, project: projects::Project) -> Result<()> {
|
||||
pub fn unwatch(&mut self, project: projects::Project) -> Result<()> {
|
||||
self.delta_watcher.unwatch(project)?;
|
||||
// TODO: how to unwatch git ?
|
||||
// TODO: how to unwatch session ?
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{events, projects, sessions, users};
|
||||
use crate::{events, projects, search, sessions, users};
|
||||
use anyhow::{Context, Result};
|
||||
use git2::Repository;
|
||||
use std::{
|
||||
@ -10,21 +10,27 @@ use std::{
|
||||
const FIVE_MINUTES: u128 = Duration::new(5 * 60, 0).as_millis();
|
||||
const ONE_HOUR: u128 = Duration::new(60 * 60, 0).as_millis();
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitWatcher {
|
||||
#[derive(Clone)]
|
||||
pub struct SessionWatcher {
|
||||
projects_storage: projects::Storage,
|
||||
users_storage: users::Storage,
|
||||
deltas_searcher: search::Deltas,
|
||||
}
|
||||
|
||||
impl GitWatcher {
|
||||
pub fn new(projects_storage: projects::Storage, users_storage: users::Storage) -> Self {
|
||||
impl<'a> SessionWatcher {
|
||||
pub fn new(
|
||||
projects_storage: projects::Storage,
|
||||
users_storage: users::Storage,
|
||||
deltas_searcher: search::Deltas,
|
||||
) -> Self {
|
||||
Self {
|
||||
projects_storage,
|
||||
users_storage,
|
||||
deltas_searcher,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&self, project_id: &str, sender: mpsc::Sender<events::Event>) -> Result<()> {
|
||||
fn run(&mut self, project_id: &str, sender: mpsc::Sender<events::Event>) -> Result<()> {
|
||||
match self
|
||||
.projects_storage
|
||||
.get_project(&project_id)
|
||||
@ -32,8 +38,6 @@ impl GitWatcher {
|
||||
format!("Error while getting project {} for git watcher", project_id)
|
||||
})? {
|
||||
Some(project) => {
|
||||
log::info!("Checking for session to commit in {}", project.path);
|
||||
|
||||
let user = self.users_storage.get().with_context(|| {
|
||||
format!(
|
||||
"Error while getting user for git watcher in {}",
|
||||
@ -67,14 +71,14 @@ impl GitWatcher {
|
||||
sender: mpsc::Sender<events::Event>,
|
||||
project: projects::Project,
|
||||
) -> Result<()> {
|
||||
log::info!("Watching git for {}", project.path);
|
||||
log::info!("Watching sessions for {}", project.path);
|
||||
|
||||
let shared_self = std::sync::Arc::new(self.clone());
|
||||
let self_copy = shared_self.clone();
|
||||
let shared_self = self.clone();
|
||||
let mut self_copy = shared_self.clone();
|
||||
let project_id = project.id;
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || loop {
|
||||
let local_self = &self_copy;
|
||||
let local_self = &mut self_copy;
|
||||
if let Err(e) = local_self.run(&project_id, sender.clone()) {
|
||||
log::error!("Error while running git watcher: {:#}", e);
|
||||
}
|
||||
@ -93,7 +97,7 @@ impl GitWatcher {
|
||||
//
|
||||
// returns a commited session if created
|
||||
fn check_for_changes(
|
||||
&self,
|
||||
&mut self,
|
||||
project: &projects::Project,
|
||||
user: &Option<users::User>,
|
||||
) -> Result<Option<sessions::Session>> {
|
||||
@ -106,6 +110,9 @@ impl GitWatcher {
|
||||
session
|
||||
.flush(&repo, user, project)
|
||||
.with_context(|| "Error while flushing session")?;
|
||||
self.deltas_searcher
|
||||
.index_session(&repo, &project, &session)
|
||||
.with_context(|| format!("Error while indexing session {}", session.id))?;
|
||||
Ok(Some(session))
|
||||
}
|
||||
}
|
||||
@ -121,10 +128,6 @@ fn session_to_commit(
|
||||
) -> Result<Option<sessions::Session>> {
|
||||
match sessions::Session::current(repo, project)? {
|
||||
None => {
|
||||
log::debug!(
|
||||
"No current session to commit for {}",
|
||||
repo.workdir().unwrap().display()
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
Some(current_session) => {
|
@ -6,7 +6,7 @@
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body class="fixed h-full w-full overflow-hidden font-sans antialiased text-base bg-zinc-800">
|
||||
<body class="fixed h-full w-full overflow-hidden bg-zinc-800 font-sans text-base antialiased">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -4,9 +4,19 @@
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3 text-zinc-400">
|
||||
<button class="p-2 rounded-md hover:text-zinc-200 hover:bg-zinc-700" title="Go back" on:click={() => history.back()}>
|
||||
<div class="w-4 h-4 flex justify-center items-center">
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<button
|
||||
class="rounded-md p-2 hover:bg-zinc-700 hover:text-zinc-200"
|
||||
title="Go back"
|
||||
on:click={() => history.back()}
|
||||
>
|
||||
<div class="flex h-4 w-4 items-center justify-center">
|
||||
<svg
|
||||
width="16"
|
||||
height="12"
|
||||
viewBox="0 0 16 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
@ -16,9 +26,19 @@
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button class="p-2 rounded-md hover:text-zinc-200 hover:bg-zinc-700" title="Go forward" on:click={() => history.forward()}>
|
||||
<div class="w-4 h-4 flex justify-center items-center">
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<button
|
||||
class="rounded-md p-2 hover:bg-zinc-700 hover:text-zinc-200"
|
||||
title="Go forward"
|
||||
on:click={() => history.forward()}
|
||||
>
|
||||
<div class="flex h-4 w-4 items-center justify-center">
|
||||
<svg
|
||||
width="16"
|
||||
height="12"
|
||||
viewBox="0 0 16 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
|
@ -12,9 +12,15 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row items-center text-zinc-400">
|
||||
<a class="p-2 rounded-md hover:text-zinc-200 hover:bg-zinc-700" href="/">
|
||||
<div class="w-4 h-4 flex justify-center items-center">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<a class="rounded-md p-2 hover:bg-zinc-700 hover:text-zinc-200" href="/">
|
||||
<div class="flex h-4 w-4 items-center justify-center">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.5547 0.16795C7.2188 -0.0559832 6.7812 -0.0559832 6.4453 0.16795L0.8906 3.87108C0.334202 4.24201 0 4.86648 0 5.53518V12C0 13.1046 0.895431 14 2 14H4C4.55228 14 5 13.5523 5 13V9H9V13C9 13.5523 9.44771 14 10 14H12C13.1046 14 14 13.1046 14 12V5.53518C14 4.86648 13.6658 4.24202 13.1094 3.87108L7.5547 0.16795Z"
|
||||
fill="#5C5F62"
|
||||
@ -25,19 +31,19 @@
|
||||
{#if $project}
|
||||
<div class="ml-1">
|
||||
<Popover>
|
||||
<div slot="button" class="flex align-item-centerh-5 py-2 px-2 rounded-md hover:bg-zinc-700">
|
||||
<div class="h-4 flex items-center">
|
||||
<div slot="button" class="align-item-centerh-5 flex rounded-md py-2 px-2 hover:bg-zinc-700">
|
||||
<div class="flex h-4 items-center">
|
||||
{$project.title}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<ul class="flex flex-col overflow-y-auto p-2 max-h-[289px]">
|
||||
<ul class="flex max-h-[289px] flex-col overflow-y-auto p-2">
|
||||
{#each $projects || [] as p}
|
||||
<a
|
||||
href="/projects/{p.id}"
|
||||
class="
|
||||
flex items-center
|
||||
p-2 rounded hover:bg-zinc-700 cursor-pointer"
|
||||
flex cursor-pointer
|
||||
items-center rounded p-2 hover:bg-zinc-700"
|
||||
>
|
||||
<span class="truncate">
|
||||
{p.title}
|
||||
@ -67,7 +73,7 @@
|
||||
<span class="w-full border-t border-zinc-700" />
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="m-2 flex">
|
||||
<a href="/" class="p-2 w-full rounded hover:bg-zinc-700 cursor-pointer"
|
||||
<a href="/" class="w-full cursor-pointer rounded p-2 hover:bg-zinc-700"
|
||||
>Add repository...</a
|
||||
>
|
||||
</div>
|
||||
|
@ -7,4 +7,4 @@
|
||||
export let filepath: string;
|
||||
</script>
|
||||
|
||||
<code class="w-full h-full" use:codeviewer={{ doc, deltas, filepath }} />
|
||||
<code class="h-full w-full" use:codeviewer={{ doc, deltas, filepath }} />
|
||||
|
@ -27,7 +27,9 @@
|
||||
|
||||
<div>
|
||||
{#if $user}
|
||||
<button class="text-zinc-400 hover:underline hover:text-red-400" on:click={() => user.delete()}>Log out</button>
|
||||
<button class="text-zinc-400 hover:text-red-400 hover:underline" on:click={() => user.delete()}
|
||||
>Log out</button
|
||||
>
|
||||
{:else if $token !== null}
|
||||
{#await Promise.all([open($token.url), pollForUser($token.token)])}
|
||||
<div>Log in in your system browser</div>
|
||||
@ -38,7 +40,7 @@
|
||||
</p>
|
||||
{:else}
|
||||
<button
|
||||
class="py-1 px-3 rounded text-white bg-blue-400"
|
||||
class="rounded bg-blue-400 py-1 px-3 text-white"
|
||||
on:click={() => api.login.token.create().then(token.set)}>Sign up or Log in</button
|
||||
>
|
||||
{/if}
|
||||
|
@ -66,7 +66,7 @@
|
||||
in:fadeAndZoomIn={{ duration: 150 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
on:mouseup={() => (showPopover = false)}
|
||||
class="wrapper z-[999] bg-zinc-800 border border-zinc-700 text-zinc-50 rounded shadow-2xl min-w-[180px] max-w-[512px]"
|
||||
class="wrapper z-[999] min-w-[180px] max-w-[512px] rounded border border-zinc-700 bg-zinc-800 text-zinc-50 shadow-2xl"
|
||||
style="--popover-top: {`${bottom}px`}; --popover-left: {`${left}px`}"
|
||||
>
|
||||
<slot />
|
||||
|
@ -53,7 +53,7 @@
|
||||
<span class="relative inline-flex">
|
||||
<a
|
||||
id="block"
|
||||
class="inline-flex flex-grow items-center truncate transition ease-in-out duration-150 border px-4 py-2 text-slate-50 rounded-lg {colorFromBranchName(
|
||||
class="inline-flex flex-grow items-center truncate rounded-lg border px-4 py-2 text-slate-50 transition duration-150 ease-in-out {colorFromBranchName(
|
||||
session.meta.branch
|
||||
)}"
|
||||
title={session.meta.branch}
|
||||
@ -62,12 +62,12 @@
|
||||
{toHumanBranchName(session.meta.branch)}
|
||||
</a>
|
||||
{#if !session.hash}
|
||||
<span class="flex absolute h-3 w-3 top-0 right-0 -mt-1 -mr-1" title="Current session">
|
||||
<span class="absolute top-0 right-0 -mt-1 -mr-1 flex h-3 w-3" title="Current session">
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-200 opacity-75"
|
||||
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-orange-200 opacity-75"
|
||||
/>
|
||||
<span
|
||||
class="relative inline-flex rounded-full h-3 w-3 bg-zinc-200 border border-orange-200"
|
||||
class="relative inline-flex h-3 w-3 rounded-full border border-orange-200 bg-zinc-200"
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
@ -91,11 +91,11 @@
|
||||
<div id="files">
|
||||
{#await list({ projectId: projectId, sessionId: session.id }) then deltas}
|
||||
{#each Object.keys(deltas) as delta}
|
||||
<div class="flex flex-row w-32 items-center">
|
||||
<div class="w-6 h-6 text-white fill-blue-400">
|
||||
<div class="flex w-32 flex-row items-center">
|
||||
<div class="h-6 w-6 fill-blue-400 text-white">
|
||||
{@html pathToIconSvg(delta)}
|
||||
</div>
|
||||
<div class="text-white w-24 truncate">
|
||||
<div class="w-24 truncate text-white">
|
||||
{pathToName(delta)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,26 +25,26 @@
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<hr class="h-px bg-slate-400 border-0 z-0" />
|
||||
<hr class="z-0 h-px border-0 bg-slate-400" />
|
||||
<div class="absolute inset-0 -mt-1.5">
|
||||
{#each activities as activity}
|
||||
<div
|
||||
class="flex -mx-1.5"
|
||||
class="-mx-1.5 flex"
|
||||
style="position:relative; left: {proportionOfTime(activity.timestampMs)}%;"
|
||||
>
|
||||
<div
|
||||
class="w-3 h-3 text-slate-700 z-50 absolute inset-0"
|
||||
class="absolute inset-0 z-50 h-3 w-3 text-slate-700"
|
||||
style=""
|
||||
title="{activity.type}: {activity.message} at {toHumanReadableTime(activity.timestampMs)}"
|
||||
>
|
||||
{#if activity.type.startsWith('commit')}
|
||||
<IconSquareRoundedFilled class="w-3 h-3 text-sky-500 hover:text-sky-600" />
|
||||
<IconSquareRoundedFilled class="h-3 w-3 text-sky-500 hover:text-sky-600" />
|
||||
{:else if activity.type.startsWith('merge')}
|
||||
<IconMapPinFilled class="w-3 h-3 text-green-500 hover:text-green-600" />
|
||||
<IconMapPinFilled class="h-3 w-3 text-green-500 hover:text-green-600" />
|
||||
{:else if activity.type.startsWith('rebase')}
|
||||
<IconCircleHalf2 class="w-3 h-3 text-orange-500 hover:text-orange-600" />
|
||||
<IconCircleHalf2 class="h-3 w-3 text-orange-500 hover:text-orange-600" />
|
||||
{:else if activity.type.startsWith('push')}
|
||||
<IconCircleFilled class="w-3 h-3 text-purple-500 hover:text-purple-600" />
|
||||
<IconCircleFilled class="h-3 w-3 text-purple-500 hover:text-purple-600" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,7 +38,7 @@
|
||||
<a
|
||||
{href}
|
||||
title={startTime.toLocaleTimeString()}
|
||||
class="group absolute inset-1 flex flex-col items-center justify-center rounded-lg bg-zinc-300 p-3 leading-5 hover:bg-zinc-200 shadow"
|
||||
class="group absolute inset-1 flex flex-col items-center justify-center rounded-lg bg-zinc-300 p-3 leading-5 shadow hover:bg-zinc-200"
|
||||
>
|
||||
<p class="order-1 font-semibold text-zinc-800">
|
||||
{toHumanBranchName(label)}
|
||||
|
@ -5,3 +5,4 @@ export * as toasts from './toasts';
|
||||
export * as sessions from './sessions';
|
||||
export * as week from './week';
|
||||
export * as uisessions from './uisessions';
|
||||
export * from './search';
|
||||
|
18
src/lib/search.ts
Normal file
18
src/lib/search.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
export type SearchResult = {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
filePath: string;
|
||||
// index of the delta in the session.
|
||||
index: number;
|
||||
timestampMsGte?: number;
|
||||
timestampMsLt?: number;
|
||||
};
|
||||
|
||||
export const search = (params: {
|
||||
projectId: string;
|
||||
query: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) => invoke<SearchResult[]>('search', params);
|
@ -15,7 +15,7 @@
|
||||
: 'Something went wrong';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-1 h-full">
|
||||
<div class="flex h-full flex-1">
|
||||
<h1 class="m-auto text-xl">
|
||||
{message} :(
|
||||
</h1>
|
||||
|
@ -18,19 +18,20 @@
|
||||
user.subscribe(posthog.identify);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col min-h-full max-h-full h-full text-zinc-400">
|
||||
<div class="flex h-full max-h-full min-h-full flex-col text-zinc-400">
|
||||
<header
|
||||
data-tauri-drag-region
|
||||
class="flex flex-row items-center border-b select-none pt-1 pb-1 text-zinc-400 border-zinc-700">
|
||||
class="flex select-none flex-row items-center border-b border-zinc-700 pt-1 pb-1 text-zinc-400"
|
||||
>
|
||||
<div class="ml-24">
|
||||
<BackForwardButtons />
|
||||
</div>
|
||||
<div class="ml-6"><Breadcrumbs /></div>
|
||||
<div class="flex-grow" />
|
||||
<a href="/users/" class="flex items-center gap-1 mr-4 font-medium hover:text-zinc-200">
|
||||
<a href="/users/" class="mr-4 flex items-center gap-1 font-medium hover:text-zinc-200">
|
||||
{#if $user}
|
||||
{#if $user.picture}
|
||||
<img class="inline-block w-5 h-5 rounded-full" src={$user.picture} alt="Avatar" />
|
||||
<img class="inline-block h-5 w-5 rounded-full" src={$user.picture} alt="Avatar" />
|
||||
{/if}
|
||||
<span>{$user.name}</span>
|
||||
{:else}
|
||||
|
@ -24,15 +24,15 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full p-8">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="h-full w-full p-8">
|
||||
<div class="flex h-full flex-col">
|
||||
{#if $projects.length == 0}
|
||||
<div class="h-fill grid grid-cols-2 gap-4 items-center h-full">
|
||||
<div class="h-fill grid h-full grid-cols-2 items-center gap-4">
|
||||
<!-- right box, welcome text -->
|
||||
<div class="flex flex-col space-y-4 content-center p-4">
|
||||
<div class="text-xl text-zinc-300 p-0 m-0">
|
||||
<div class="flex flex-col content-center space-y-4 p-4">
|
||||
<div class="m-0 p-0 text-xl text-zinc-300">
|
||||
<div class="font-bold">Welcome to GitButler.</div>
|
||||
<div class="text-lg text-zinc-300 mb-1">More than just version control.</div>
|
||||
<div class="mb-1 text-lg text-zinc-300">More than just version control.</div>
|
||||
</div>
|
||||
<div class="">
|
||||
GitButler is a tool to help you manage all the local work you do on your code projects.
|
||||
@ -41,7 +41,7 @@
|
||||
Think of us as a <strong>code concierge</strong>, a smart assistant for all the coding
|
||||
related tasks you need to do every day.
|
||||
</div>
|
||||
<ul class="text-zinc-400 pt-2 pb-4 space-y-4">
|
||||
<ul class="space-y-4 pt-2 pb-4 text-zinc-400">
|
||||
<li class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -49,7 +49,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-8 h-8 flex-none"
|
||||
class="h-8 w-8 flex-none"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -69,7 +69,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-8 h-8 flex-none"
|
||||
class="h-8 w-8 flex-none"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -90,7 +90,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-8 h-8 flex-none"
|
||||
class="h-8 w-8 flex-none"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -110,7 +110,7 @@
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
href="https://help.gitbutler.com"
|
||||
class="text-base font-semibold leading-7 text-white bg-zinc-700 px-4 py-3 rounded-lg mt-4"
|
||||
class="mt-4 rounded-lg bg-zinc-700 px-4 py-3 text-base font-semibold leading-7 text-white"
|
||||
>
|
||||
Learn more <span aria-hidden="true">→</span></a
|
||||
>
|
||||
@ -150,9 +150,9 @@
|
||||
<div class="select-none p-8">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xl text-zinc-300 mb-1">
|
||||
<div class="mb-1 text-xl text-zinc-300">
|
||||
My Projects
|
||||
<div class="text-lg text-zinc-500 mb-1">
|
||||
<div class="mb-1 text-lg text-zinc-500">
|
||||
All the projects that I am currently assisting you with.
|
||||
</div>
|
||||
</div>
|
||||
@ -167,32 +167,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full max-h-screen overflow-auto">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each $projects as project}
|
||||
<a
|
||||
class="hover:text-zinc-200 text-zinc-300 text-lg"
|
||||
href="/projects/{project.id}/">
|
||||
<div class="flex flex-col justify-between space-y-1 bg-zinc-700 rounded-lg shadow">
|
||||
<div class="px-4 py-4 flex-grow-0">
|
||||
|
||||
<div class="hover:text-zinc-200 text-zinc-300 text-lg">
|
||||
<a class="text-lg text-zinc-300 hover:text-zinc-200" href="/projects/{project.id}/">
|
||||
<div
|
||||
class="flex flex-col justify-between space-y-1 rounded-lg bg-zinc-700 shadow"
|
||||
>
|
||||
<div class="flex-grow-0 px-4 py-4">
|
||||
<div class="text-lg text-zinc-300 hover:text-zinc-200">
|
||||
{project.title}
|
||||
</div>
|
||||
<div class="text-zinc-500 font-mono break-words">
|
||||
<div class="font-mono break-words text-zinc-500">
|
||||
{project.path}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex-grow-0 text-zinc-500 font-mono border-t border-zinc-600 bg-zinc-600 rounded-b-lg px-3 py-1"
|
||||
class="font-mono flex-grow-0 rounded-b-lg border-t border-zinc-600 bg-zinc-600 px-3 py-1 text-zinc-500"
|
||||
>
|
||||
{#if project.api}
|
||||
<div class="flex flex-row items-center space-x-2 ">
|
||||
<div class="w-2 h-2 bg-green-700 rounded-full" />
|
||||
<div class="h-2 w-2 rounded-full bg-green-700" />
|
||||
<div class="text-zinc-400">syncing</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row items-center space-x-2 ">
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full" />
|
||||
<div class="h-2 w-2 rounded-full bg-gray-400" />
|
||||
<div class="text-zinc-400">offline</div>
|
||||
</div>
|
||||
{/if}
|
||||
@ -206,9 +205,7 @@
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 w-full">
|
||||
<div
|
||||
class="flex items-center flex-shrink-0 p-4 h-18 border-t select-none border-zinc-700"
|
||||
>
|
||||
<div class="h-18 flex flex-shrink-0 select-none items-center border-t border-zinc-700 p-4">
|
||||
<div class="text-sm text-zinc-300">Timeline</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,16 +29,16 @@
|
||||
$: selection = $page?.route?.id?.split('/')?.[3];
|
||||
</script>
|
||||
|
||||
<div class="flex w-full h-full flex-col">
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<nav
|
||||
class="flex items-center flex-none justify-between py-1 px-8 space-x-3 border-b select-none text-zinc-300 border-zinc-700"
|
||||
class="flex flex-none select-none items-center justify-between space-x-3 border-b border-zinc-700 py-1 px-8 text-zinc-300"
|
||||
>
|
||||
<div />
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
|
||||
<div class="p-1 rounded-md hover:text-zinc-200 hover:bg-zinc-700 hover:bg-zinc-700">
|
||||
<div class="rounded-md p-1 hover:bg-zinc-700 hover:bg-zinc-700 hover:text-zinc-200">
|
||||
<div class="h-6 w-6 ">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -46,7 +46,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -72,13 +72,13 @@
|
||||
|
||||
<footer class="w-full text-sm font-medium">
|
||||
<div
|
||||
class="flex items-center flex-shrink-0 h-6 border-t select-none border-zinc-700 bg-zinc-800 "
|
||||
class="flex h-6 flex-shrink-0 select-none items-center border-t border-zinc-700 bg-zinc-800 "
|
||||
>
|
||||
<div class="flex flex-row mx-4 items-center space-x-2 justify-between w-full">
|
||||
<div class="mx-4 flex w-full flex-row items-center justify-between space-x-2">
|
||||
{#if $project?.api?.sync}
|
||||
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
|
||||
<div class="flex flex-row items-center space-x-2 ">
|
||||
<div class="w-2 h-2 bg-green-700 rounded-full" />
|
||||
<div class="h-2 w-2 rounded-full bg-green-700" />
|
||||
<div>Syncing</div>
|
||||
</div>
|
||||
</a>
|
||||
@ -87,7 +87,7 @@
|
||||
{:else}
|
||||
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
|
||||
<div class="flex flex-row items-center space-x-2 ">
|
||||
<div class="w-2 h-2 bg-red-700 rounded-full" />
|
||||
<div class="h-2 w-2 rounded-full bg-red-700" />
|
||||
<div>Offline</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const prerender = false;
|
||||
export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
const { projects } = await parent();
|
||||
return {
|
||||
|
@ -5,13 +5,16 @@
|
||||
$: project = data.project;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col mt-12">
|
||||
<h1 class="text-zinc-200 text-xl flex justify-center">
|
||||
<div class="mt-12 flex flex-col">
|
||||
<h1 class="flex justify-center text-xl text-zinc-200">
|
||||
Overview of {$project?.title}
|
||||
</h1>
|
||||
<div class="flex justify-center space-x-2 text-lg">
|
||||
<a href="/projects/{$project?.id}/timeline" class="hover:text-zinc-200 text-orange-400"
|
||||
<a href="/projects/{$project?.id}/timeline" class="text-orange-400 hover:text-zinc-200"
|
||||
>Timeline</a
|
||||
>
|
||||
<a href="/projects/{$project?.id}/search" class="text-orange-400 hover:text-zinc-200"
|
||||
>search (test)</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
38
src/routes/projects/[projectId]/search/+page.svelte
Normal file
38
src/routes/projects/[projectId]/search/+page.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { search, type SearchResult } from '$lib';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let data: PageData;
|
||||
const { project } = data;
|
||||
|
||||
let query: string;
|
||||
|
||||
const results = writable<SearchResult[]>([]);
|
||||
|
||||
const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
const fetchResults = debounce(async () => {
|
||||
if (!$project) return;
|
||||
if (!query) return results.set([]);
|
||||
search({ projectId: $project.id, query }).then(results.set);
|
||||
}, 100);
|
||||
</script>
|
||||
|
||||
<figure class="flex flex-col gap-2">
|
||||
<figcaption>
|
||||
<input on:input={fetchResults} type="text" name="query" bind:value={query} />
|
||||
</figcaption>
|
||||
|
||||
<ul class="gap-q flex flex-col">
|
||||
{#each $results as result}
|
||||
<li>{JSON.stringify(result)}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</figure>
|
@ -78,9 +78,9 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="p-4 mx-auto h-full overflow-auto">
|
||||
<div class="max-w-2xl mx-auto p-4">
|
||||
<div class="flex flex-col text-zinc-100 space-y-6">
|
||||
<div class="mx-auto h-full overflow-auto p-4">
|
||||
<div class="mx-auto max-w-2xl p-4">
|
||||
<div class="flex flex-col space-y-6 text-zinc-100">
|
||||
<div class="space-y-0">
|
||||
<div class="text-xl font-medium">Project Settings</div>
|
||||
<div class="text-zinc-400">
|
||||
@ -92,7 +92,7 @@
|
||||
<div class="space-y-2">
|
||||
<div class="ml-1">GitButler Cloud</div>
|
||||
<div
|
||||
class="flex flex-row justify-between border border-zinc-600 rounded-lg p-2 items-center"
|
||||
class="flex flex-row items-center justify-between rounded-lg border border-zinc-600 p-2"
|
||||
>
|
||||
<div class="flex flex-row space-x-3">
|
||||
<svg
|
||||
@ -101,7 +101,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -113,11 +113,11 @@
|
||||
{#if $project?.api?.git_url}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-zinc-300">Git Host</div>
|
||||
<div class="text-zinc-400 font-mono">
|
||||
<div class="font-mono text-zinc-400">
|
||||
{hostname($project?.api?.git_url)}
|
||||
</div>
|
||||
<div class="text-zinc-300 mt-3">Repository ID</div>
|
||||
<div class="text-zinc-400 font-mono">
|
||||
<div class="mt-3 text-zinc-300">Repository ID</div>
|
||||
<div class="font-mono text-zinc-400">
|
||||
{repo_id($project?.api?.git_url)}
|
||||
</div>
|
||||
</div>
|
||||
@ -140,7 +140,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-row space-x-2 items-end">
|
||||
<div class="flex flex-row items-end space-x-2">
|
||||
<div class="">GitButler Cloud</div>
|
||||
<div class="text-zinc-400">backup your work and access advanced features</div>
|
||||
</div>
|
||||
@ -158,7 +158,7 @@
|
||||
id="path"
|
||||
name="path"
|
||||
type="text"
|
||||
class="p-2 text-zinc-300 bg-zinc-900 border border-zinc-600 rounded-lg w-full"
|
||||
class="w-full rounded-lg border border-zinc-600 bg-black p-2 text-zinc-300"
|
||||
value={$project?.path}
|
||||
/>
|
||||
</div>
|
||||
@ -168,7 +168,7 @@
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
class="p-2 text-zinc-300 bg-zinc-900 border border-zinc-600 rounded-lg w-full"
|
||||
class="w-full rounded-lg border border-zinc-600 bg-black p-2 text-zinc-300"
|
||||
value={$project?.title}
|
||||
required
|
||||
/>
|
||||
@ -180,6 +180,7 @@
|
||||
name="description"
|
||||
rows="3"
|
||||
class="p-2 text-zinc-300 bg-zinc-900 border border-zinc-600 rounded-lg w-full"
|
||||
class="w-full rounded-lg border border-zinc-600 bg-black p-2 text-zinc-300"
|
||||
value={$project?.api?.description}
|
||||
/>
|
||||
</div>
|
||||
@ -188,13 +189,13 @@
|
||||
<footer>
|
||||
{#if saving}
|
||||
<div
|
||||
class="flex w-32 flex-row w-content items-center gap-1 justify-center py-2 px-3 rounded text-white bg-blue-400"
|
||||
class="w-content flex w-32 flex-row items-center justify-center gap-1 rounded bg-blue-400 py-2 px-3 text-white"
|
||||
>
|
||||
<IconRotateClockwise2 class="w-5 h-5 animate-spin" />
|
||||
<IconRotateClockwise2 class="h-5 w-5 animate-spin" />
|
||||
<span>Updating...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="py-2 px-3 rounded text-white bg-blue-600"
|
||||
<button type="submit" class="rounded bg-blue-600 py-2 px-3 text-white"
|
||||
>Update profile</button
|
||||
>
|
||||
{/if}
|
||||
|
@ -151,29 +151,31 @@
|
||||
{#if $dateSessions === undefined}
|
||||
<span>Loading...</span>
|
||||
{:else}
|
||||
<div class="h-full flex flex-row space-x-12 px-4 py-4 pb-6">
|
||||
<div class="flex h-full flex-row space-x-12 px-4 py-4 pb-6">
|
||||
{#each Object.entries($dateSessions) as [dateMilliseconds, uiSessions]}
|
||||
<!-- Day -->
|
||||
<div
|
||||
id={dateMilliseconds}
|
||||
class="session-day-component flex flex-col bg-zinc-800/50 rounded-lg border border-zinc-700"
|
||||
class="session-day-component flex flex-col rounded-lg border border-zinc-700 bg-zinc-800/50"
|
||||
class:min-w-full={selection.dateMilliseconds == +dateMilliseconds}
|
||||
>
|
||||
<div class="session-day-container font-medium border-b border-zinc-700 bg-zinc-700/30 flex items-center py-2 px-4">
|
||||
<span class="session-day-header text-zinc-200 font-bold">
|
||||
<div
|
||||
class="session-day-container flex items-center border-b border-zinc-700 bg-zinc-700/30 py-2 px-4 font-medium"
|
||||
>
|
||||
<span class="session-day-header font-bold text-zinc-200">
|
||||
{formatDate(new Date(+dateMilliseconds))}
|
||||
</span>
|
||||
</div>
|
||||
{#if selection.dateMilliseconds !== +dateMilliseconds}
|
||||
<div class="flex flex-col flex-auto">
|
||||
<div class="h-2/3 flex space-x-2 p-3">
|
||||
<div class="flex flex-auto flex-col">
|
||||
<div class="flex h-2/3 space-x-2 p-3">
|
||||
{#each uiSessions as uiSession, i}
|
||||
<!-- Session (overview) -->
|
||||
|
||||
<div class="session-column-container flex flex-col w-40">
|
||||
<div class="session-column-container flex w-40 flex-col">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="repository-name cursor-pointer text-sm text-center font-bold rounded borded text-zinc-800 p-1 border bg-orange-400 border-orange-400 hover:bg-[#fdbc87]"
|
||||
class="repository-name borded cursor-pointer rounded border border-orange-400 bg-orange-400 p-1 text-center text-sm font-bold text-zinc-800 hover:bg-[#fdbc87]"
|
||||
on:click={() => expandSession(i, uiSession, +dateMilliseconds)}
|
||||
>
|
||||
{toHumanBranchName(uiSession.session.meta.branch)}
|
||||
@ -190,12 +192,12 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col p-1" id="sessions-details">
|
||||
<div class="text-zinc-400 font-medium">
|
||||
<div class="font-medium text-zinc-400">
|
||||
{formatTime(new Date(uiSession.earliestDeltaTimestampMs))}
|
||||
-
|
||||
{formatTime(new Date(uiSession.latestDeltaTimestampMs))}
|
||||
</div>
|
||||
<div class="text-zinc-500 text-sm" title="Session duration">
|
||||
<div class="text-sm text-zinc-500" title="Session duration">
|
||||
{Math.round(
|
||||
(uiSession.latestDeltaTimestampMs - uiSession.earliestDeltaTimestampMs) /
|
||||
1000 /
|
||||
@ -206,12 +208,12 @@
|
||||
{#each Object.keys(uiSession.deltas) as filePath}
|
||||
<button
|
||||
on:click={() => expandSession(i, uiSession, +dateMilliseconds, filePath)}
|
||||
class="cursor-pointer flex flex-row w-32 items-center"
|
||||
class="flex w-32 cursor-pointer flex-row items-center"
|
||||
>
|
||||
<div class="w-6 h-6 text-zinc-200 fill-blue-400">
|
||||
<div class="h-6 w-6 fill-blue-400 text-zinc-200">
|
||||
{@html pathToIconSvg(filePath)}
|
||||
</div>
|
||||
<div class= "file-name text-zinc-300 hover:text-zinc-50 w-24 truncate">
|
||||
<div class="file-name w-24 truncate text-zinc-300 hover:text-zinc-50">
|
||||
{pathToName(filePath)}
|
||||
</div>
|
||||
</button>
|
||||
@ -221,12 +223,12 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="day-summary-container h-1/3 p-4 border-t border-zinc-400 ">
|
||||
<div class="day-summary-container h-1/3 border-t border-zinc-400 p-4 ">
|
||||
<div class="day-summary-header font-bold text-zinc-200">Day summary</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="my-2 flex-auto overflow-auto flex flex-row space-x-2">
|
||||
<div class="my-2 flex flex-auto flex-row space-x-2 overflow-auto">
|
||||
<div class="">
|
||||
<button
|
||||
on:click={() => {
|
||||
@ -240,14 +242,14 @@
|
||||
}}
|
||||
class="{selection.sessionIdx == 0
|
||||
? 'disabled cursor-default brightness-50'
|
||||
: 'hover:bg-[#fdbc87]'} rounded-r bg-orange-400 border border-orange-400 text-zinc-800 p-1 text-center text-sm font-medium "
|
||||
: 'hover:bg-[#fdbc87]'} rounded-r border border-orange-400 bg-orange-400 p-1 text-center text-sm font-medium text-zinc-800 "
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-full flex flex-col border rounded-t border-orange-400">
|
||||
<div class="flex w-full flex-col rounded-t border border-orange-400">
|
||||
<div
|
||||
class="session-header px-4 bg-orange-400 border border-orange-400 p-1 rounded-t-sm text-zinc-800 text-sm font-bold flex items-center justify-between"
|
||||
class="session-header flex items-center justify-between rounded-t-sm border border-orange-400 bg-orange-400 p-1 px-4 text-sm font-bold text-zinc-800"
|
||||
>
|
||||
<span class="cursor-default"
|
||||
>{format(selection.start, 'hh:mm')} - {format(selection.end, 'hh:mm')}</span
|
||||
@ -255,11 +257,9 @@
|
||||
<span>{toHumanBranchName(selection.branch)}</span>
|
||||
<button on:click={resetSelection}>Close</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-auto overflow-auto flex flex-col">
|
||||
<div
|
||||
class="timeline-container shadow shadow-zinc-700 ring-1 ring-zinc-700 ring-opacity-5 mb-1">
|
||||
<div class="grid-cols-11 -mr-px border-zinc-700 grid text-xs font-medium">
|
||||
<div class="timeline-container flex flex-auto flex-col overflow-auto">
|
||||
<div class="mb-1 shadow shadow-zinc-700 ring-1 ring-zinc-700 ring-opacity-5">
|
||||
<div class="-mr-px grid grid-cols-11 border-zinc-700 text-xs font-medium">
|
||||
<div class="col-span-2 flex items-center justify-center py-1">
|
||||
<span>{format(selection.start, 'hh:mm')}</span>
|
||||
</div>
|
||||
@ -300,7 +300,7 @@
|
||||
<!-- needle -->
|
||||
<div class="grid grid-cols-11">
|
||||
<div class="col-span-2 flex items-center justify-center" />
|
||||
<div class="-mx-1 col-span-8 flex items-center justify-center">
|
||||
<div class="col-span-8 -mx-1 flex items-center justify-center">
|
||||
<Slider min={17} max={80} step={1} bind:value={selection.selectedColumn}>
|
||||
<svelte:fragment slot="tooltip" let:value>
|
||||
{format(colToTimestamp(value, selection.start, selection.end), 'hh:mm')}
|
||||
@ -323,15 +323,15 @@
|
||||
{#each Object.keys(selection.deltas) as filePath}
|
||||
<div
|
||||
class="flex {filePath === selection.selectedFilePath
|
||||
? 'bg-blue-500/70 font-bold rounded-sm mx-1'
|
||||
? 'mx-1 rounded-sm bg-blue-500/70 font-bold'
|
||||
: ''}"
|
||||
>
|
||||
<button
|
||||
class="text-xs z-20 flex justify-end items-center overflow-hidden sticky left-0 w-1/6 leading-5
|
||||
class="sticky left-0 z-20 flex w-1/6 items-center justify-end overflow-hidden text-xs leading-5
|
||||
{selection.selectedFilePath ===
|
||||
filePath
|
||||
? 'text-zinc-200 cursor-default'
|
||||
: 'text-zinc-400 hover:text-zinc-200 cursor-pointer'}"
|
||||
? 'cursor-default text-zinc-200'
|
||||
: 'cursor-pointer text-zinc-400 hover:text-zinc-200'}"
|
||||
on:click={() => (selection.selectedFilePath = filePath)}
|
||||
title={filePath}
|
||||
>
|
||||
@ -353,7 +353,7 @@
|
||||
</div>
|
||||
<!-- time vertical lines -->
|
||||
<div
|
||||
class="col-start-1 col-end-2 row-start-1 grid-rows-1 divide-x divide-zinc-700/50 grid grid-cols-11"
|
||||
class="col-start-1 col-end-2 row-start-1 grid grid-cols-11 grid-rows-1 divide-x divide-zinc-700/50"
|
||||
>
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
@ -375,7 +375,7 @@
|
||||
{#each Object.entries(selection.deltas) as [filePath, fileDeltas], idx}
|
||||
{#each fileDeltas as delta}
|
||||
<li
|
||||
class="relative flex items-center bg-zinc-300 hover:bg-zinc-100 rounded m-0.5 cursor-pointer"
|
||||
class="relative m-0.5 flex cursor-pointer items-center rounded bg-zinc-300 hover:bg-zinc-100"
|
||||
style="
|
||||
grid-row: {idx +
|
||||
1} / span 1;
|
||||
@ -386,7 +386,7 @@
|
||||
)} / span 1;"
|
||||
>
|
||||
<button
|
||||
class="z-20 flex flex-col w-full items-center justify-center"
|
||||
class="z-20 flex w-full flex-col items-center justify-center"
|
||||
on:click={() => {
|
||||
selection.selectedColumn = timeStampToCol(
|
||||
new Date(delta.timestampMs),
|
||||
@ -428,7 +428,7 @@
|
||||
}}
|
||||
class="{selection.sessionIdx < uiSessions.length - 1
|
||||
? 'hover:bg-[#fdbc87]'
|
||||
: 'disabled cursor-default brightness-50'} rounded-r bg-orange-400 border border-orange-400 text-zinc-800 p-1 text-center text-sm font-medium "
|
||||
: 'disabled cursor-default brightness-50'} rounded-r border border-orange-400 bg-orange-400 p-1 text-center text-sm font-medium text-zinc-800 "
|
||||
>
|
||||
›
|
||||
</button>
|
||||
|
@ -4,7 +4,6 @@ import { building } from '$app/environment';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import type { UISession } from '$lib/uisessions';
|
||||
import { asyncDerived } from '@square/svelte-store';
|
||||
import { list as listDeltas } from '$lib/deltas';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import { startOfDay } from 'date-fns';
|
||||
|
||||
@ -18,53 +17,57 @@ export const load: PageLoad = async ({ parent, params }) => {
|
||||
return sessions.slice().sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs);
|
||||
});
|
||||
|
||||
const dateSessions = asyncDerived([orderedSessions], async ([sessions]) => {
|
||||
const deltas = await Promise.all(
|
||||
sessions.map((session) => {
|
||||
return listDeltas({
|
||||
projectId: params.projectId ?? '',
|
||||
sessionId: session.id
|
||||
let dateSessions = readable<Record<number, UISession[]>>({});
|
||||
if (!building) {
|
||||
const listDeltas = (await import('$lib/deltas')).list;
|
||||
dateSessions = asyncDerived([orderedSessions], async ([sessions]) => {
|
||||
const deltas = await Promise.all(
|
||||
sessions.map((session) => {
|
||||
return listDeltas({
|
||||
projectId: params.projectId ?? '',
|
||||
sessionId: session.id
|
||||
});
|
||||
})
|
||||
);
|
||||
// Sort deltas by timestamp
|
||||
deltas.forEach((delta) => {
|
||||
Object.keys(delta).forEach((key) => {
|
||||
delta[key].sort((a, b) => a.timestampMs - b.timestampMs);
|
||||
});
|
||||
})
|
||||
);
|
||||
// Sort deltas by timestamp
|
||||
deltas.forEach((delta) => {
|
||||
Object.keys(delta).forEach((key) => {
|
||||
delta[key].sort((a, b) => a.timestampMs - b.timestampMs);
|
||||
});
|
||||
});
|
||||
|
||||
const uiSessions = sessions
|
||||
.map((session, i) => {
|
||||
return { session, deltas: deltas[i] } as UISession;
|
||||
})
|
||||
.filter((uiSession) => {
|
||||
return Object.keys(uiSession.deltas).length > 0;
|
||||
});
|
||||
|
||||
const dateSessions: Record<number, UISession[]> = {};
|
||||
uiSessions.forEach((uiSession) => {
|
||||
const date = startOfDay(new Date(uiSession.session.meta.startTimestampMs));
|
||||
if (dateSessions[date.getTime()]) {
|
||||
dateSessions[date.getTime()]?.push(uiSession);
|
||||
} else {
|
||||
dateSessions[date.getTime()] = [uiSession];
|
||||
}
|
||||
});
|
||||
const uiSessions = sessions
|
||||
.map((session, i) => {
|
||||
return { session, deltas: deltas[i] } as UISession;
|
||||
})
|
||||
.filter((uiSession) => {
|
||||
return Object.keys(uiSession.deltas).length > 0;
|
||||
});
|
||||
|
||||
// For each UISession in dateSessions, set the earliestDeltaTimestampMs and latestDeltaTimestampMs
|
||||
Object.keys(dateSessions).forEach((date: any) => {
|
||||
dateSessions[date].forEach((uiSession: any) => {
|
||||
const deltaTimestamps = Object.keys(uiSession.deltas).reduce((acc, key) => {
|
||||
return acc.concat(uiSession.deltas[key].map((delta: Delta) => delta.timestampMs));
|
||||
}, []);
|
||||
uiSession.earliestDeltaTimestampMs = Math.min(...deltaTimestamps);
|
||||
uiSession.latestDeltaTimestampMs = Math.max(...deltaTimestamps);
|
||||
const dateSessions: Record<number, UISession[]> = {};
|
||||
uiSessions.forEach((uiSession) => {
|
||||
const date = startOfDay(new Date(uiSession.session.meta.startTimestampMs));
|
||||
if (dateSessions[date.getTime()]) {
|
||||
dateSessions[date.getTime()]?.push(uiSession);
|
||||
} else {
|
||||
dateSessions[date.getTime()] = [uiSession];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return dateSessions;
|
||||
});
|
||||
// For each UISession in dateSessions, set the earliestDeltaTimestampMs and latestDeltaTimestampMs
|
||||
Object.keys(dateSessions).forEach((date: any) => {
|
||||
dateSessions[date].forEach((uiSession: any) => {
|
||||
const deltaTimestamps = Object.keys(uiSession.deltas).reduce((acc, key) => {
|
||||
return acc.concat(uiSession.deltas[key].map((delta: Delta) => delta.timestampMs));
|
||||
}, []);
|
||||
uiSession.earliestDeltaTimestampMs = Math.min(...deltaTimestamps);
|
||||
uiSession.latestDeltaTimestampMs = Math.max(...deltaTimestamps);
|
||||
});
|
||||
});
|
||||
|
||||
return dateSessions;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
project: project,
|
||||
|
@ -54,8 +54,8 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="p-4 mx-auto">
|
||||
<div class="max-w-xl mx-auto p-4">
|
||||
<div class="mx-auto p-4">
|
||||
<div class="mx-auto max-w-xl p-4">
|
||||
{#if $user}
|
||||
<div class="flex flex-col gap-6 text-zinc-100">
|
||||
<header class="flex items-center justify-between">
|
||||
@ -68,7 +68,7 @@
|
||||
|
||||
<form
|
||||
on:submit={onSubmit}
|
||||
class="flex flex-row gap-12 justify-between rounded-lg p-2 items-start"
|
||||
class="flex flex-row items-start justify-between gap-12 rounded-lg p-2"
|
||||
>
|
||||
<fields id="left" class="flex flex-1 flex-col gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
@ -78,7 +78,7 @@
|
||||
name="name"
|
||||
bind:value={userName}
|
||||
type="text"
|
||||
class="px-2 py-2 text-zinc-300 bg-zinc-900 border border-zinc-600 rounded-lg w-full"
|
||||
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -91,24 +91,29 @@
|
||||
name="email"
|
||||
bind:value={$user.email}
|
||||
type="text"
|
||||
class="px-2 py-2 text-zinc-300 bg-zinc-900 border border-zinc-600 rounded-lg w-full"
|
||||
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer class="pt-4">
|
||||
{#if saving}
|
||||
<div
|
||||
class="flex w-32 flex-row w-content items-center gap-1 justify-center px-4 py-2 rounded text-white bg-blue-600">
|
||||
<IconRotateClockwise2 class="w-5 h-5 animate-spin" />
|
||||
class="w-content flex w-32 flex-row items-center justify-center gap-1 rounded bg-blue-600 px-4 py-2 text-white"
|
||||
>
|
||||
<IconRotateClockwise2 class="h-5 w-5 animate-spin" />
|
||||
<span>Updating...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="px-4 py-2 rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none">Update profile</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:outline-none"
|
||||
>Update profile</button
|
||||
>
|
||||
{/if}
|
||||
</footer>
|
||||
</fields>
|
||||
|
||||
<fields id="right" class="flex flex-col gap-2 items-center">
|
||||
<fields id="right" class="flex flex-col items-center gap-2">
|
||||
{#if $user.picture}
|
||||
<img
|
||||
class="h-28 w-28 rounded-full border-zinc-300"
|
||||
@ -119,7 +124,8 @@
|
||||
|
||||
<label
|
||||
for="picture"
|
||||
class="px-2 -mt-6 -ml-16 cursor-pointer text-center font-sm text-zinc-300 bg-zinc-800 border border-zinc-600 rounded-lg hover:text-zinc-50 bg-zinc-800 hover:bg-zinc-900">
|
||||
class="font-sm -mt-6 -ml-16 cursor-pointer rounded-lg border border-zinc-600 bg-zinc-800 bg-zinc-800 px-2 text-center text-zinc-300 hover:bg-zinc-900 hover:text-zinc-50"
|
||||
>
|
||||
Edit
|
||||
<input
|
||||
on:change={onPictureChange}
|
||||
@ -134,10 +140,10 @@
|
||||
</form>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col text-white space-y-6 items-center justify-items-center">
|
||||
<div class="flex flex-col items-center justify-items-center space-y-6 text-white">
|
||||
<div class="text-3xl font-bold text-white">Connect to GitButler Cloud</div>
|
||||
<div>Sign up or log in to GitButler Cloud for more tools and features:</div>
|
||||
<ul class="text-zinc-400 pb-4 space-y-2">
|
||||
<ul class="space-y-2 pb-4 text-zinc-400">
|
||||
<li class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -145,7 +151,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -162,7 +168,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -180,7 +186,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -194,7 +200,7 @@
|
||||
<div class="mt-8 text-center">
|
||||
<Login {user} {api} />
|
||||
</div>
|
||||
<div class="text-zinc-300 text-center">
|
||||
<div class="text-center text-zinc-300">
|
||||
You will still need to give us permission for each project before we transfer any data to
|
||||
our servers. You can revoke this permission at any time.
|
||||
</div>
|
||||
@ -202,7 +208,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col mt-8 border-t border-zinc-400 pt-4">
|
||||
<h2 class="text-lg text-zinc-100 font-bold">Get Support</h2>
|
||||
<h2 class="text-lg text-zinc-100 font-medium">Get Support</h2>
|
||||
<div class="text-sm text-zinc-300">
|
||||
If you have an issue or any questions, please email us.
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user