Merge branch 'master' into visual-design-updates

This commit is contained in:
Ian Donahue 2023-03-07 13:37:00 +01:00
commit e7341094b8
41 changed files with 1637 additions and 430 deletions

View File

@ -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" } }]
}

View File

@ -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",

View File

@ -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
View File

@ -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"

View File

@ -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

View File

@ -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![];

View File

@ -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 {

View File

@ -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);

View File

@ -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() {

View 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)
}

View 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);
}

View File

@ -0,0 +1,6 @@
mod deltas;
pub use deltas::{Deltas, SearchQuery, SearchResult};
#[cfg(test)]
mod deltas_test;

View File

@ -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;

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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(())
}
}

View File

@ -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) => {

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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 }} />

View File

@ -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}

View File

@ -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 />

View File

@ -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>

View File

@ -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>

View File

@ -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)}

View File

@ -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
View 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);

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View 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>

View File

@ -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}

View File

@ -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>

View File

@ -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,

View File

@ -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>