diff --git a/Cargo.lock b/Cargo.lock index fc172379725..71b3112a0e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2356,6 +2356,7 @@ dependencies = [ "smallvec", "thiserror 2.0.18", "tokio", + "unicode-normalization", "zeroize", ] diff --git a/bindings/electron/src/index.d.ts b/bindings/electron/src/index.d.ts index cc69b0fa5d2..37e163a86a1 100644 --- a/bindings/electron/src/index.d.ts +++ b/bindings/electron/src/index.d.ts @@ -501,6 +501,14 @@ export interface WorkspaceHistoryFileStat { } +export interface WorkspaceHistorySearchMatch { + path: string + stat: WorkspaceHistoryEntryStat + score: number + matchPositions: Array +} + + export interface WorkspaceInfo { id: string currentName: string @@ -510,6 +518,14 @@ export interface WorkspaceInfo { } +export interface WorkspaceSearchMatch { + path: string + stat: EntryStat + score: number + matchPositions: Array +} + + export interface WorkspaceUserAccessInfo { userId: string humanHandle: HumanHandle @@ -4289,6 +4305,33 @@ export type WorkspaceHistoryRealmExportDecryptor = | WorkspaceHistoryRealmExportDecryptorUser +// WorkspaceHistorySearchCloseError +export interface WorkspaceHistorySearchCloseErrorInternal { + tag: "WorkspaceHistorySearchCloseErrorInternal" + error: string +} +export type WorkspaceHistorySearchCloseError = + | WorkspaceHistorySearchCloseErrorInternal + + +// WorkspaceHistorySearchError +export interface WorkspaceHistorySearchErrorInternal { + tag: "WorkspaceHistorySearchErrorInternal" + error: string +} +export type WorkspaceHistorySearchError = + | WorkspaceHistorySearchErrorInternal + + +// WorkspaceHistorySearchGetNextError +export interface WorkspaceHistorySearchGetNextErrorInternal { + tag: "WorkspaceHistorySearchGetNextErrorInternal" + error: string +} +export type WorkspaceHistorySearchGetNextError = + | WorkspaceHistorySearchGetNextErrorInternal + + // WorkspaceHistorySetTimestampOfInterestError export interface WorkspaceHistorySetTimestampOfInterestErrorEntryNotFound { tag: "WorkspaceHistorySetTimestampOfInterestErrorEntryNotFound" @@ -4779,6 +4822,33 @@ export type WorkspaceRemoveEntryError = | WorkspaceRemoveEntryErrorStopped +// WorkspaceSearchCloseError +export interface WorkspaceSearchCloseErrorInternal { + tag: "WorkspaceSearchCloseErrorInternal" + error: string +} +export type WorkspaceSearchCloseError = + | WorkspaceSearchCloseErrorInternal + + +// WorkspaceSearchError +export interface WorkspaceSearchErrorInternal { + tag: "WorkspaceSearchErrorInternal" + error: string +} +export type WorkspaceSearchError = + | WorkspaceSearchErrorInternal + + +// WorkspaceSearchGetNextError +export interface WorkspaceSearchGetNextErrorInternal { + tag: "WorkspaceSearchGetNextErrorInternal" + error: string +} +export type WorkspaceSearchGetNextError = + | WorkspaceSearchGetNextErrorInternal + + // WorkspaceStatEntryError export interface WorkspaceStatEntryErrorEntryNotFound { tag: "WorkspaceStatEntryErrorEntryNotFound" @@ -5660,6 +5730,17 @@ export function workspaceHistoryOpenFileById( workspace_history: number, entry_id: string ): Promise> +export function workspaceHistorySearch( + workspace_history: number, + path: string, + query: string +): Promise> +export function workspaceHistorySearchClose( + search: number +): Promise> +export function workspaceHistorySearchGetNext( + search: number +): Promise> export function workspaceHistorySetTimestampOfInterest( workspace_history: number, toi: number @@ -5742,6 +5823,17 @@ export function workspaceRenameEntryById( dst_name: string, mode: MoveEntryMode ): Promise> +export function workspaceSearch( + workspace: number, + path: string, + query: string +): Promise> +export function workspaceSearchClose( + search: number +): Promise> +export function workspaceSearchGetNext( + search: number +): Promise> export function workspaceStatEntry( workspace: number, path: string diff --git a/bindings/electron/src/meths.rs b/bindings/electron/src/meths.rs index fe226bfa177..99026a1548d 100644 --- a/bindings/electron/src/meths.rs +++ b/bindings/electron/src/meths.rs @@ -4888,6 +4888,100 @@ fn struct_workspace_history_file_stat_rs_to_js<'a>( Ok(js_obj) } +// WorkspaceHistorySearchMatch + +#[allow(dead_code)] +fn struct_workspace_history_search_match_js_to_rs<'a>( + cx: &mut impl Context<'a>, + obj: Handle<'a, JsObject>, +) -> NeonResult { + let path = { + let js_val: Handle = obj.get(cx, "path")?; + { + let custom_from_rs_string = |s: String| -> Result<_, String> { + s.parse::().map_err(|e| e.to_string()) + }; + match custom_from_rs_string(js_val.value(cx)) { + Ok(val) => val, + Err(err) => return cx.throw_type_error(err), + } + } + }; + let stat = { + let js_val: Handle = obj.get(cx, "stat")?; + variant_workspace_history_entry_stat_js_to_rs(cx, js_val)? + }; + let score = { + let js_val: Handle = obj.get(cx, "score")?; + { + let v = js_val.value(cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + } + }; + let match_positions = { + let js_val: Handle = obj.get(cx, "matchPositions")?; + { + let size = js_val.len(cx); + let mut v = Vec::with_capacity(size as usize); + for i in 0..size { + let js_item: Handle = js_val.get(cx, i)?; + v.push({ + let v = js_item.value(cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + }); + } + v + } + }; + Ok(libparsec::WorkspaceHistorySearchMatch { + path, + stat, + score, + match_positions, + }) +} + +#[allow(dead_code)] +fn struct_workspace_history_search_match_rs_to_js<'a>( + cx: &mut impl Context<'a>, + rs_obj: libparsec::WorkspaceHistorySearchMatch, +) -> NeonResult> { + let js_obj = cx.empty_object(); + let js_path = JsString::try_new(cx, { + let custom_to_rs_string = + |v| -> Result<_, std::convert::Infallible> { Ok(std::string::ToString::to_string(&v)) }; + match custom_to_rs_string(rs_obj.path) { + Ok(ok) => ok, + Err(err) => return cx.throw_type_error(err.to_string()), + } + }) + .or_throw(cx)?; + js_obj.set(cx, "path", js_path)?; + let js_stat = variant_workspace_history_entry_stat_rs_to_js(cx, rs_obj.stat)?; + js_obj.set(cx, "stat", js_stat)?; + let js_score = JsNumber::new(cx, rs_obj.score as f64); + js_obj.set(cx, "score", js_score)?; + let js_match_positions = { + // JsArray::new allocates with `undefined` value, that's why we `set` value + let js_array = JsArray::new(cx, rs_obj.match_positions.len()); + for (i, elem) in rs_obj.match_positions.into_iter().enumerate() { + let js_elem = JsNumber::new(cx, elem as f64); + js_array.set(cx, i as u32, js_elem)?; + } + js_array + }; + js_obj.set(cx, "matchPositions", js_match_positions)?; + Ok(js_obj) +} + // WorkspaceInfo #[allow(dead_code)] @@ -4971,6 +5065,100 @@ fn struct_workspace_info_rs_to_js<'a>( Ok(js_obj) } +// WorkspaceSearchMatch + +#[allow(dead_code)] +fn struct_workspace_search_match_js_to_rs<'a>( + cx: &mut impl Context<'a>, + obj: Handle<'a, JsObject>, +) -> NeonResult { + let path = { + let js_val: Handle = obj.get(cx, "path")?; + { + let custom_from_rs_string = |s: String| -> Result<_, String> { + s.parse::().map_err(|e| e.to_string()) + }; + match custom_from_rs_string(js_val.value(cx)) { + Ok(val) => val, + Err(err) => return cx.throw_type_error(err), + } + } + }; + let stat = { + let js_val: Handle = obj.get(cx, "stat")?; + variant_entry_stat_js_to_rs(cx, js_val)? + }; + let score = { + let js_val: Handle = obj.get(cx, "score")?; + { + let v = js_val.value(cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + } + }; + let match_positions = { + let js_val: Handle = obj.get(cx, "matchPositions")?; + { + let size = js_val.len(cx); + let mut v = Vec::with_capacity(size as usize); + for i in 0..size { + let js_item: Handle = js_val.get(cx, i)?; + v.push({ + let v = js_item.value(cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + }); + } + v + } + }; + Ok(libparsec::WorkspaceSearchMatch { + path, + stat, + score, + match_positions, + }) +} + +#[allow(dead_code)] +fn struct_workspace_search_match_rs_to_js<'a>( + cx: &mut impl Context<'a>, + rs_obj: libparsec::WorkspaceSearchMatch, +) -> NeonResult> { + let js_obj = cx.empty_object(); + let js_path = JsString::try_new(cx, { + let custom_to_rs_string = + |v| -> Result<_, std::convert::Infallible> { Ok(std::string::ToString::to_string(&v)) }; + match custom_to_rs_string(rs_obj.path) { + Ok(ok) => ok, + Err(err) => return cx.throw_type_error(err.to_string()), + } + }) + .or_throw(cx)?; + js_obj.set(cx, "path", js_path)?; + let js_stat = variant_entry_stat_rs_to_js(cx, rs_obj.stat)?; + js_obj.set(cx, "stat", js_stat)?; + let js_score = JsNumber::new(cx, rs_obj.score as f64); + js_obj.set(cx, "score", js_score)?; + let js_match_positions = { + // JsArray::new allocates with `undefined` value, that's why we `set` value + let js_array = JsArray::new(cx, rs_obj.match_positions.len()); + for (i, elem) in rs_obj.match_positions.into_iter().enumerate() { + let js_elem = JsNumber::new(cx, elem as f64); + js_array.set(cx, i as u32, js_elem)?; + } + js_array + }; + js_obj.set(cx, "matchPositions", js_match_positions)?; + Ok(js_obj) +} + // WorkspaceUserAccessInfo #[allow(dead_code)] @@ -17057,6 +17245,66 @@ fn variant_workspace_history_realm_export_decryptor_rs_to_js<'a>( Ok(js_obj) } +// WorkspaceHistorySearchCloseError + +#[allow(dead_code)] +fn variant_workspace_history_search_close_error_rs_to_js<'a>( + cx: &mut impl Context<'a>, + rs_obj: libparsec::WorkspaceHistorySearchCloseError, +) -> NeonResult> { + let js_obj = cx.empty_object(); + let js_display = JsString::try_new(cx, &rs_obj.to_string()).or_throw(cx)?; + js_obj.set(cx, "error", js_display)?; + match rs_obj { + libparsec::WorkspaceHistorySearchCloseError::Internal { .. } => { + let js_tag = + JsString::try_new(cx, "WorkspaceHistorySearchCloseErrorInternal").or_throw(cx)?; + js_obj.set(cx, "tag", js_tag)?; + } + } + Ok(js_obj) +} + +// WorkspaceHistorySearchError + +#[allow(dead_code)] +fn variant_workspace_history_search_error_rs_to_js<'a>( + cx: &mut impl Context<'a>, + rs_obj: libparsec::WorkspaceHistorySearchError, +) -> NeonResult> { + let js_obj = cx.empty_object(); + let js_display = JsString::try_new(cx, &rs_obj.to_string()).or_throw(cx)?; + js_obj.set(cx, "error", js_display)?; + match rs_obj { + libparsec::WorkspaceHistorySearchError::Internal { .. } => { + let js_tag = + JsString::try_new(cx, "WorkspaceHistorySearchErrorInternal").or_throw(cx)?; + js_obj.set(cx, "tag", js_tag)?; + } + } + Ok(js_obj) +} + +// WorkspaceHistorySearchGetNextError + +#[allow(dead_code)] +fn variant_workspace_history_search_get_next_error_rs_to_js<'a>( + cx: &mut impl Context<'a>, + rs_obj: libparsec::WorkspaceHistorySearchGetNextError, +) -> NeonResult> { + let js_obj = cx.empty_object(); + let js_display = JsString::try_new(cx, &rs_obj.to_string()).or_throw(cx)?; + js_obj.set(cx, "error", js_display)?; + match rs_obj { + libparsec::WorkspaceHistorySearchGetNextError::Internal { .. } => { + let js_tag = + JsString::try_new(cx, "WorkspaceHistorySearchGetNextErrorInternal").or_throw(cx)?; + js_obj.set(cx, "tag", js_tag)?; + } + } + Ok(js_obj) +} + // WorkspaceHistorySetTimestampOfInterestError #[allow(dead_code)] @@ -17692,6 +17940,64 @@ fn variant_workspace_remove_entry_error_rs_to_js<'a>( Ok(js_obj) } +// WorkspaceSearchCloseError + +#[allow(dead_code)] +fn variant_workspace_search_close_error_rs_to_js<'a>( + cx: &mut impl Context<'a>, + rs_obj: libparsec::WorkspaceSearchCloseError, +) -> NeonResult> { + let js_obj = cx.empty_object(); + let js_display = JsString::try_new(cx, &rs_obj.to_string()).or_throw(cx)?; + js_obj.set(cx, "error", js_display)?; + match rs_obj { + libparsec::WorkspaceSearchCloseError::Internal { .. } => { + let js_tag = JsString::try_new(cx, "WorkspaceSearchCloseErrorInternal").or_throw(cx)?; + js_obj.set(cx, "tag", js_tag)?; + } + } + Ok(js_obj) +} + +// WorkspaceSearchError + +#[allow(dead_code)] +fn variant_workspace_search_error_rs_to_js<'a>( + cx: &mut impl Context<'a>, + rs_obj: libparsec::WorkspaceSearchError, +) -> NeonResult> { + let js_obj = cx.empty_object(); + let js_display = JsString::try_new(cx, &rs_obj.to_string()).or_throw(cx)?; + js_obj.set(cx, "error", js_display)?; + match rs_obj { + libparsec::WorkspaceSearchError::Internal { .. } => { + let js_tag = JsString::try_new(cx, "WorkspaceSearchErrorInternal").or_throw(cx)?; + js_obj.set(cx, "tag", js_tag)?; + } + } + Ok(js_obj) +} + +// WorkspaceSearchGetNextError + +#[allow(dead_code)] +fn variant_workspace_search_get_next_error_rs_to_js<'a>( + cx: &mut impl Context<'a>, + rs_obj: libparsec::WorkspaceSearchGetNextError, +) -> NeonResult> { + let js_obj = cx.empty_object(); + let js_display = JsString::try_new(cx, &rs_obj.to_string()).or_throw(cx)?; + js_obj.set(cx, "error", js_display)?; + match rs_obj { + libparsec::WorkspaceSearchGetNextError::Internal { .. } => { + let js_tag = + JsString::try_new(cx, "WorkspaceSearchGetNextErrorInternal").or_throw(cx)?; + js_obj.set(cx, "tag", js_tag)?; + } + } + Ok(js_obj) +} + // WorkspaceStatEntryError #[allow(dead_code)] @@ -28741,6 +29047,171 @@ fn workspace_history_open_file_by_id(mut cx: FunctionContext) -> JsResult JsResult { + crate::init_sentry(); + let workspace_history = { + let js_val = cx.argument::(0)?; + { + let v = js_val.value(&mut cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + } + }; + let path = { + let js_val = cx.argument::(1)?; + { + let custom_from_rs_string = |s: String| -> Result<_, String> { + s.parse::().map_err(|e| e.to_string()) + }; + match custom_from_rs_string(js_val.value(&mut cx)) { + Ok(val) => val, + Err(err) => return cx.throw_type_error(err), + } + } + }; + let query = { + let js_val = cx.argument::(2)?; + js_val.value(&mut cx) + }; + let channel = cx.channel(); + let (deferred, promise) = cx.promise(); + + // TODO: Promises are not cancellable in Javascript by default, should we add a custom cancel method ? + let _handle = crate::TOKIO_RUNTIME + .lock() + .expect("Mutex is poisoned") + .spawn(async move { + let ret = libparsec::workspace_history_search(workspace_history, path, &query).await; + + deferred.settle_with(&channel, move |mut cx| { + let js_ret = match ret { + Ok(ok) => { + let js_obj = JsObject::new(&mut cx); + let js_tag = JsBoolean::new(&mut cx, true); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_value = JsNumber::new(&mut cx, ok as f64); + js_obj.set(&mut cx, "value", js_value)?; + js_obj + } + Err(err) => { + let js_obj = cx.empty_object(); + let js_tag = JsBoolean::new(&mut cx, false); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_err = variant_workspace_history_search_error_rs_to_js(&mut cx, err)?; + js_obj.set(&mut cx, "error", js_err)?; + js_obj + } + }; + Ok(js_ret) + }); + }); + + Ok(promise) +} + +// workspace_history_search_close +fn workspace_history_search_close(mut cx: FunctionContext) -> JsResult { + crate::init_sentry(); + let search = { + let js_val = cx.argument::(0)?; + { + let v = js_val.value(&mut cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + } + }; + let ret = libparsec::workspace_history_search_close(search); + let js_ret = match ret { + Ok(ok) => { + let js_obj = JsObject::new(&mut cx); + let js_tag = JsBoolean::new(&mut cx, true); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_value = { + #[allow(clippy::let_unit_value)] + let _ = ok; + JsNull::new(&mut cx) + }; + js_obj.set(&mut cx, "value", js_value)?; + js_obj + } + Err(err) => { + let js_obj = cx.empty_object(); + let js_tag = JsBoolean::new(&mut cx, false); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_err = variant_workspace_history_search_close_error_rs_to_js(&mut cx, err)?; + js_obj.set(&mut cx, "error", js_err)?; + js_obj + } + }; + let (deferred, promise) = cx.promise(); + deferred.resolve(&mut cx, js_ret); + Ok(promise) +} + +// workspace_history_search_get_next +fn workspace_history_search_get_next(mut cx: FunctionContext) -> JsResult { + crate::init_sentry(); + let search = { + let js_val = cx.argument::(0)?; + { + let v = js_val.value(&mut cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + } + }; + let channel = cx.channel(); + let (deferred, promise) = cx.promise(); + + // TODO: Promises are not cancellable in Javascript by default, should we add a custom cancel method ? + let _handle = crate::TOKIO_RUNTIME + .lock() + .expect("Mutex is poisoned") + .spawn(async move { + let ret = libparsec::workspace_history_search_get_next(search).await; + + deferred.settle_with(&channel, move |mut cx| { + let js_ret = match ret { + Ok(ok) => { + let js_obj = JsObject::new(&mut cx); + let js_tag = JsBoolean::new(&mut cx, true); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_value = match ok { + Some(elem) => { + struct_workspace_history_search_match_rs_to_js(&mut cx, elem)? + .as_value(&mut cx) + } + None => JsNull::new(&mut cx).as_value(&mut cx), + }; + js_obj.set(&mut cx, "value", js_value)?; + js_obj + } + Err(err) => { + let js_obj = cx.empty_object(); + let js_tag = JsBoolean::new(&mut cx, false); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_err = + variant_workspace_history_search_get_next_error_rs_to_js(&mut cx, err)?; + js_obj.set(&mut cx, "error", js_err)?; + js_obj + } + }; + Ok(js_ret) + }); + }); + + Ok(promise) +} + // workspace_history_set_timestamp_of_interest fn workspace_history_set_timestamp_of_interest(mut cx: FunctionContext) -> JsResult { crate::init_sentry(); @@ -30099,6 +30570,169 @@ fn workspace_rename_entry_by_id(mut cx: FunctionContext) -> JsResult Ok(promise) } +// workspace_search +fn workspace_search(mut cx: FunctionContext) -> JsResult { + crate::init_sentry(); + let workspace = { + let js_val = cx.argument::(0)?; + { + let v = js_val.value(&mut cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + } + }; + let path = { + let js_val = cx.argument::(1)?; + { + let custom_from_rs_string = |s: String| -> Result<_, String> { + s.parse::().map_err(|e| e.to_string()) + }; + match custom_from_rs_string(js_val.value(&mut cx)) { + Ok(val) => val, + Err(err) => return cx.throw_type_error(err), + } + } + }; + let query = { + let js_val = cx.argument::(2)?; + js_val.value(&mut cx) + }; + let channel = cx.channel(); + let (deferred, promise) = cx.promise(); + + // TODO: Promises are not cancellable in Javascript by default, should we add a custom cancel method ? + let _handle = crate::TOKIO_RUNTIME + .lock() + .expect("Mutex is poisoned") + .spawn(async move { + let ret = libparsec::workspace_search(workspace, path, &query).await; + + deferred.settle_with(&channel, move |mut cx| { + let js_ret = match ret { + Ok(ok) => { + let js_obj = JsObject::new(&mut cx); + let js_tag = JsBoolean::new(&mut cx, true); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_value = JsNumber::new(&mut cx, ok as f64); + js_obj.set(&mut cx, "value", js_value)?; + js_obj + } + Err(err) => { + let js_obj = cx.empty_object(); + let js_tag = JsBoolean::new(&mut cx, false); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_err = variant_workspace_search_error_rs_to_js(&mut cx, err)?; + js_obj.set(&mut cx, "error", js_err)?; + js_obj + } + }; + Ok(js_ret) + }); + }); + + Ok(promise) +} + +// workspace_search_close +fn workspace_search_close(mut cx: FunctionContext) -> JsResult { + crate::init_sentry(); + let search = { + let js_val = cx.argument::(0)?; + { + let v = js_val.value(&mut cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + } + }; + let ret = libparsec::workspace_search_close(search); + let js_ret = match ret { + Ok(ok) => { + let js_obj = JsObject::new(&mut cx); + let js_tag = JsBoolean::new(&mut cx, true); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_value = { + #[allow(clippy::let_unit_value)] + let _ = ok; + JsNull::new(&mut cx) + }; + js_obj.set(&mut cx, "value", js_value)?; + js_obj + } + Err(err) => { + let js_obj = cx.empty_object(); + let js_tag = JsBoolean::new(&mut cx, false); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_err = variant_workspace_search_close_error_rs_to_js(&mut cx, err)?; + js_obj.set(&mut cx, "error", js_err)?; + js_obj + } + }; + let (deferred, promise) = cx.promise(); + deferred.resolve(&mut cx, js_ret); + Ok(promise) +} + +// workspace_search_get_next +fn workspace_search_get_next(mut cx: FunctionContext) -> JsResult { + crate::init_sentry(); + let search = { + let js_val = cx.argument::(0)?; + { + let v = js_val.value(&mut cx); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + cx.throw_type_error("Not an u32 number")? + } + let v = v as u32; + v + } + }; + let channel = cx.channel(); + let (deferred, promise) = cx.promise(); + + // TODO: Promises are not cancellable in Javascript by default, should we add a custom cancel method ? + let _handle = crate::TOKIO_RUNTIME + .lock() + .expect("Mutex is poisoned") + .spawn(async move { + let ret = libparsec::workspace_search_get_next(search).await; + + deferred.settle_with(&channel, move |mut cx| { + let js_ret = match ret { + Ok(ok) => { + let js_obj = JsObject::new(&mut cx); + let js_tag = JsBoolean::new(&mut cx, true); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_value = match ok { + Some(elem) => struct_workspace_search_match_rs_to_js(&mut cx, elem)? + .as_value(&mut cx), + None => JsNull::new(&mut cx).as_value(&mut cx), + }; + js_obj.set(&mut cx, "value", js_value)?; + js_obj + } + Err(err) => { + let js_obj = cx.empty_object(); + let js_tag = JsBoolean::new(&mut cx, false); + js_obj.set(&mut cx, "ok", js_tag)?; + let js_err = + variant_workspace_search_get_next_error_rs_to_js(&mut cx, err)?; + js_obj.set(&mut cx, "error", js_err)?; + js_obj + } + }; + Ok(js_ret) + }); + }); + + Ok(promise) +} + // workspace_stat_entry fn workspace_stat_entry(mut cx: FunctionContext) -> JsResult { crate::init_sentry(); @@ -30983,6 +31617,15 @@ pub fn register_meths(cx: &mut ModuleContext) -> NeonResult<()> { "workspaceHistoryOpenFileById", workspace_history_open_file_by_id, )?; + cx.export_function("workspaceHistorySearch", workspace_history_search)?; + cx.export_function( + "workspaceHistorySearchClose", + workspace_history_search_close, + )?; + cx.export_function( + "workspaceHistorySearchGetNext", + workspace_history_search_get_next, + )?; cx.export_function( "workspaceHistorySetTimestampOfInterest", workspace_history_set_timestamp_of_interest, @@ -31020,6 +31663,9 @@ pub fn register_meths(cx: &mut ModuleContext) -> NeonResult<()> { cx.export_function("workspaceRemoveFolder", workspace_remove_folder)?; cx.export_function("workspaceRemoveFolderAll", workspace_remove_folder_all)?; cx.export_function("workspaceRenameEntryById", workspace_rename_entry_by_id)?; + cx.export_function("workspaceSearch", workspace_search)?; + cx.export_function("workspaceSearchClose", workspace_search_close)?; + cx.export_function("workspaceSearchGetNext", workspace_search_get_next)?; cx.export_function("workspaceStatEntry", workspace_stat_entry)?; cx.export_function("workspaceStatEntryById", workspace_stat_entry_by_id)?; cx.export_function( diff --git a/bindings/generator/api/workspace.py b/bindings/generator/api/workspace.py index 11c4e7e1603..96859c87f3f 100644 --- a/bindings/generator/api/workspace.py +++ b/bindings/generator/api/workspace.py @@ -6,6 +6,7 @@ ParsecWorkspacePathAddrAndRedirectionURL, ) from .common import ( + U32, U64, DateTime, DeviceID, @@ -774,3 +775,45 @@ async def workspace_decrypt_path_addr( link: Ref[ParsecWorkspacePathAddr], ) -> Result[FsPath, WorkspaceDecryptPathAddrError]: raise NotImplementedError + + +class WorkspaceSearchMatch(Structure): + path: FsPath + stat: EntryStat + score: U32 + match_positions: list[U32] + + +class WorkspaceSearchError(ErrorVariant): + class Internal: + pass + + +async def workspace_search( + workspace: Handle, + path: FsPath, + query: Ref[str], +) -> Result[Handle, WorkspaceSearchError]: + raise NotImplementedError + + +class WorkspaceSearchGetNextError(ErrorVariant): + class Internal: + pass + + +async def workspace_search_get_next( + search: Handle, +) -> Result[WorkspaceSearchMatch | None, WorkspaceSearchGetNextError]: + raise NotImplementedError + + +class WorkspaceSearchCloseError(ErrorVariant): + class Internal: + pass + + +def workspace_search_close( + search: Handle, +) -> Result[None, WorkspaceSearchCloseError]: + raise NotImplementedError diff --git a/bindings/generator/api/workspace_history.py b/bindings/generator/api/workspace_history.py index 6628b315b65..b44f9d8995e 100644 --- a/bindings/generator/api/workspace_history.py +++ b/bindings/generator/api/workspace_history.py @@ -2,6 +2,7 @@ from .client import ClientConfig, DeviceAccessStrategy from .common import ( + U32, U64, DateTime, DeviceID, @@ -408,3 +409,45 @@ async def workspace_history_fd_stat( fd: FileDescriptor, ) -> Result[WorkspaceHistoryFileStat, WorkspaceHistoryFdStatError]: raise NotImplementedError + + +class WorkspaceHistorySearchMatch(Structure): + path: FsPath + stat: WorkspaceHistoryEntryStat + score: U32 + match_positions: list[U32] + + +class WorkspaceHistorySearchError(ErrorVariant): + class Internal: + pass + + +async def workspace_history_search( + workspace_history: Handle, + path: FsPath, + query: Ref[str], +) -> Result[Handle, WorkspaceHistorySearchError]: + raise NotImplementedError + + +class WorkspaceHistorySearchGetNextError(ErrorVariant): + class Internal: + pass + + +async def workspace_history_search_get_next( + search: Handle, +) -> Result[WorkspaceHistorySearchMatch | None, WorkspaceHistorySearchGetNextError]: + raise NotImplementedError + + +class WorkspaceHistorySearchCloseError(ErrorVariant): + class Internal: + pass + + +def workspace_history_search_close( + search: Handle, +) -> Result[None, WorkspaceHistorySearchCloseError]: + raise NotImplementedError diff --git a/bindings/web/src/meths.rs b/bindings/web/src/meths.rs index c4d116e9b63..47558eb6d38 100644 --- a/bindings/web/src/meths.rs +++ b/bindings/web/src/meths.rs @@ -5114,6 +5114,108 @@ fn struct_workspace_history_file_stat_rs_to_js( Ok(js_obj) } +// WorkspaceHistorySearchMatch + +#[allow(dead_code)] +fn struct_workspace_history_search_match_js_to_rs( + obj: JsValue, +) -> Result { + let path = { + let js_val = Reflect::get(&obj, &"path".into())?; + js_val + .dyn_into::() + .ok() + .and_then(|s| s.as_string()) + .ok_or_else(|| TypeError::new("Not a string")) + .and_then(|x| { + let custom_from_rs_string = |s: String| -> Result<_, String> { + s.parse::().map_err(|e| e.to_string()) + }; + custom_from_rs_string(x).map_err(|e| TypeError::new(e.as_ref())) + })? + }; + let stat = { + let js_val = Reflect::get(&obj, &"stat".into())?; + variant_workspace_history_entry_stat_js_to_rs(js_val)? + }; + let score = { + let js_val = Reflect::get(&obj, &"score".into())?; + { + let v = js_val + .dyn_into::() + .map_err(|_| TypeError::new("Not a number"))? + .value_of(); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + return Err(JsValue::from(TypeError::new("Not an u32 number"))); + } + let v = v as u32; + v + } + }; + let match_positions = { + let js_val = Reflect::get(&obj, &"matchPositions".into())?; + { + let js_val = js_val + .dyn_into::() + .map_err(|_| TypeError::new("Not an array"))?; + let mut converted = Vec::with_capacity(js_val.length() as usize); + for x in js_val.iter() { + let x_converted = { + let v = x + .dyn_into::() + .map_err(|_| TypeError::new("Not a number"))? + .value_of(); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + return Err(JsValue::from(TypeError::new("Not an u32 number"))); + } + let v = v as u32; + v + }; + converted.push(x_converted); + } + converted + } + }; + Ok(libparsec::WorkspaceHistorySearchMatch { + path, + stat, + score, + match_positions, + }) +} + +#[allow(dead_code)] +fn struct_workspace_history_search_match_rs_to_js( + rs_obj: libparsec::WorkspaceHistorySearchMatch, +) -> Result { + let js_obj = Object::new().into(); + let js_path = JsValue::from_str({ + let custom_to_rs_string = + |v| -> Result<_, std::convert::Infallible> { Ok(std::string::ToString::to_string(&v)) }; + match custom_to_rs_string(rs_obj.path) { + Ok(ok) => ok, + Err(err) => return Err(JsValue::from(TypeError::new(&err.to_string()))), + } + .as_ref() + }); + Reflect::set(&js_obj, &"path".into(), &js_path)?; + let js_stat = variant_workspace_history_entry_stat_rs_to_js(rs_obj.stat)?; + Reflect::set(&js_obj, &"stat".into(), &js_stat)?; + let js_score = JsValue::from(rs_obj.score); + Reflect::set(&js_obj, &"score".into(), &js_score)?; + let js_match_positions = { + // Array::new_with_length allocates with `undefined` value, that's why we `set` value + let js_array = Array::new_with_length(rs_obj.match_positions.len() as u32); + for (i, elem) in rs_obj.match_positions.into_iter().enumerate() { + let js_elem = JsValue::from(elem); + js_array.set(i as u32, js_elem); + } + js_array.into() + }; + Reflect::set(&js_obj, &"matchPositions".into(), &js_match_positions)?; + Ok(js_obj) +} + // WorkspaceInfo #[allow(dead_code)] @@ -5205,6 +5307,108 @@ fn struct_workspace_info_rs_to_js(rs_obj: libparsec::WorkspaceInfo) -> Result Result { + let path = { + let js_val = Reflect::get(&obj, &"path".into())?; + js_val + .dyn_into::() + .ok() + .and_then(|s| s.as_string()) + .ok_or_else(|| TypeError::new("Not a string")) + .and_then(|x| { + let custom_from_rs_string = |s: String| -> Result<_, String> { + s.parse::().map_err(|e| e.to_string()) + }; + custom_from_rs_string(x).map_err(|e| TypeError::new(e.as_ref())) + })? + }; + let stat = { + let js_val = Reflect::get(&obj, &"stat".into())?; + variant_entry_stat_js_to_rs(js_val)? + }; + let score = { + let js_val = Reflect::get(&obj, &"score".into())?; + { + let v = js_val + .dyn_into::() + .map_err(|_| TypeError::new("Not a number"))? + .value_of(); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + return Err(JsValue::from(TypeError::new("Not an u32 number"))); + } + let v = v as u32; + v + } + }; + let match_positions = { + let js_val = Reflect::get(&obj, &"matchPositions".into())?; + { + let js_val = js_val + .dyn_into::() + .map_err(|_| TypeError::new("Not an array"))?; + let mut converted = Vec::with_capacity(js_val.length() as usize); + for x in js_val.iter() { + let x_converted = { + let v = x + .dyn_into::() + .map_err(|_| TypeError::new("Not a number"))? + .value_of(); + if v < (u32::MIN as f64) || (u32::MAX as f64) < v { + return Err(JsValue::from(TypeError::new("Not an u32 number"))); + } + let v = v as u32; + v + }; + converted.push(x_converted); + } + converted + } + }; + Ok(libparsec::WorkspaceSearchMatch { + path, + stat, + score, + match_positions, + }) +} + +#[allow(dead_code)] +fn struct_workspace_search_match_rs_to_js( + rs_obj: libparsec::WorkspaceSearchMatch, +) -> Result { + let js_obj = Object::new().into(); + let js_path = JsValue::from_str({ + let custom_to_rs_string = + |v| -> Result<_, std::convert::Infallible> { Ok(std::string::ToString::to_string(&v)) }; + match custom_to_rs_string(rs_obj.path) { + Ok(ok) => ok, + Err(err) => return Err(JsValue::from(TypeError::new(&err.to_string()))), + } + .as_ref() + }); + Reflect::set(&js_obj, &"path".into(), &js_path)?; + let js_stat = variant_entry_stat_rs_to_js(rs_obj.stat)?; + Reflect::set(&js_obj, &"stat".into(), &js_stat)?; + let js_score = JsValue::from(rs_obj.score); + Reflect::set(&js_obj, &"score".into(), &js_score)?; + let js_match_positions = { + // Array::new_with_length allocates with `undefined` value, that's why we `set` value + let js_array = Array::new_with_length(rs_obj.match_positions.len() as u32); + for (i, elem) in rs_obj.match_positions.into_iter().enumerate() { + let js_elem = JsValue::from(elem); + js_array.set(i as u32, js_elem); + } + js_array.into() + }; + Reflect::set(&js_obj, &"matchPositions".into(), &js_match_positions)?; + Ok(js_obj) +} + // WorkspaceUserAccessInfo #[allow(dead_code)] @@ -18831,6 +19035,69 @@ fn variant_workspace_history_realm_export_decryptor_rs_to_js( Ok(js_obj) } +// WorkspaceHistorySearchCloseError + +#[allow(dead_code)] +fn variant_workspace_history_search_close_error_rs_to_js( + rs_obj: libparsec::WorkspaceHistorySearchCloseError, +) -> Result { + let js_obj = Object::new().into(); + let js_display = &rs_obj.to_string(); + Reflect::set(&js_obj, &"error".into(), &js_display.into())?; + match rs_obj { + libparsec::WorkspaceHistorySearchCloseError::Internal { .. } => { + Reflect::set( + &js_obj, + &"tag".into(), + &"WorkspaceHistorySearchCloseErrorInternal".into(), + )?; + } + } + Ok(js_obj) +} + +// WorkspaceHistorySearchError + +#[allow(dead_code)] +fn variant_workspace_history_search_error_rs_to_js( + rs_obj: libparsec::WorkspaceHistorySearchError, +) -> Result { + let js_obj = Object::new().into(); + let js_display = &rs_obj.to_string(); + Reflect::set(&js_obj, &"error".into(), &js_display.into())?; + match rs_obj { + libparsec::WorkspaceHistorySearchError::Internal { .. } => { + Reflect::set( + &js_obj, + &"tag".into(), + &"WorkspaceHistorySearchErrorInternal".into(), + )?; + } + } + Ok(js_obj) +} + +// WorkspaceHistorySearchGetNextError + +#[allow(dead_code)] +fn variant_workspace_history_search_get_next_error_rs_to_js( + rs_obj: libparsec::WorkspaceHistorySearchGetNextError, +) -> Result { + let js_obj = Object::new().into(); + let js_display = &rs_obj.to_string(); + Reflect::set(&js_obj, &"error".into(), &js_display.into())?; + match rs_obj { + libparsec::WorkspaceHistorySearchGetNextError::Internal { .. } => { + Reflect::set( + &js_obj, + &"tag".into(), + &"WorkspaceHistorySearchGetNextErrorInternal".into(), + )?; + } + } + Ok(js_obj) +} + // WorkspaceHistorySetTimestampOfInterestError #[allow(dead_code)] @@ -19597,6 +19864,69 @@ fn variant_workspace_remove_entry_error_rs_to_js( Ok(js_obj) } +// WorkspaceSearchCloseError + +#[allow(dead_code)] +fn variant_workspace_search_close_error_rs_to_js( + rs_obj: libparsec::WorkspaceSearchCloseError, +) -> Result { + let js_obj = Object::new().into(); + let js_display = &rs_obj.to_string(); + Reflect::set(&js_obj, &"error".into(), &js_display.into())?; + match rs_obj { + libparsec::WorkspaceSearchCloseError::Internal { .. } => { + Reflect::set( + &js_obj, + &"tag".into(), + &"WorkspaceSearchCloseErrorInternal".into(), + )?; + } + } + Ok(js_obj) +} + +// WorkspaceSearchError + +#[allow(dead_code)] +fn variant_workspace_search_error_rs_to_js( + rs_obj: libparsec::WorkspaceSearchError, +) -> Result { + let js_obj = Object::new().into(); + let js_display = &rs_obj.to_string(); + Reflect::set(&js_obj, &"error".into(), &js_display.into())?; + match rs_obj { + libparsec::WorkspaceSearchError::Internal { .. } => { + Reflect::set( + &js_obj, + &"tag".into(), + &"WorkspaceSearchErrorInternal".into(), + )?; + } + } + Ok(js_obj) +} + +// WorkspaceSearchGetNextError + +#[allow(dead_code)] +fn variant_workspace_search_get_next_error_rs_to_js( + rs_obj: libparsec::WorkspaceSearchGetNextError, +) -> Result { + let js_obj = Object::new().into(); + let js_display = &rs_obj.to_string(); + Reflect::set(&js_obj, &"error".into(), &js_display.into())?; + match rs_obj { + libparsec::WorkspaceSearchGetNextError::Internal { .. } => { + Reflect::set( + &js_obj, + &"tag".into(), + &"WorkspaceSearchGetNextErrorInternal".into(), + )?; + } + } + Ok(js_obj) +} + // WorkspaceStatEntryError #[allow(dead_code)] @@ -26034,6 +26364,94 @@ pub fn workspaceHistoryOpenFileById(workspace_history: u32, entry_id: String) -> })) } +// workspace_history_search +#[allow(non_snake_case)] +#[wasm_bindgen] +pub fn workspaceHistorySearch(workspace_history: u32, path: String, query: String) -> Promise { + future_to_promise(libparsec::WithTaskIDFuture::from(async move { + let path = { + let custom_from_rs_string = |s: String| -> Result<_, String> { + s.parse::().map_err(|e| e.to_string()) + }; + custom_from_rs_string(path).map_err(|e| TypeError::new(e.as_ref())) + }?; + + let ret = libparsec::workspace_history_search(workspace_history, path, &query).await; + Ok(match ret { + Ok(value) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &true.into())?; + let js_value = JsValue::from(value); + Reflect::set(&js_obj, &"value".into(), &js_value)?; + js_obj + } + Err(err) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &false.into())?; + let js_err = variant_workspace_history_search_error_rs_to_js(err)?; + Reflect::set(&js_obj, &"error".into(), &js_err)?; + js_obj + } + }) + })) +} + +// workspace_history_search_close +#[allow(non_snake_case)] +#[wasm_bindgen] +pub fn workspaceHistorySearchClose(search: u32) -> Promise { + future_to_promise(libparsec::WithTaskIDFuture::from(async move { + let ret = libparsec::workspace_history_search_close(search); + Ok(match ret { + Ok(value) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &true.into())?; + let js_value = { + let _ = value; + JsValue::null() + }; + Reflect::set(&js_obj, &"value".into(), &js_value)?; + js_obj + } + Err(err) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &false.into())?; + let js_err = variant_workspace_history_search_close_error_rs_to_js(err)?; + Reflect::set(&js_obj, &"error".into(), &js_err)?; + js_obj + } + }) + })) +} + +// workspace_history_search_get_next +#[allow(non_snake_case)] +#[wasm_bindgen] +pub fn workspaceHistorySearchGetNext(search: u32) -> Promise { + future_to_promise(libparsec::WithTaskIDFuture::from(async move { + let ret = libparsec::workspace_history_search_get_next(search).await; + Ok(match ret { + Ok(value) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &true.into())?; + let js_value = match value { + Some(val) => struct_workspace_history_search_match_rs_to_js(val)?, + None => JsValue::NULL, + }; + Reflect::set(&js_obj, &"value".into(), &js_value)?; + js_obj + } + Err(err) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &false.into())?; + let js_err = variant_workspace_history_search_get_next_error_rs_to_js(err)?; + Reflect::set(&js_obj, &"error".into(), &js_err)?; + js_obj + } + }) + })) +} + // workspace_history_set_timestamp_of_interest #[allow(non_snake_case)] #[wasm_bindgen] @@ -26801,6 +27219,94 @@ pub fn workspaceRenameEntryById( })) } +// workspace_search +#[allow(non_snake_case)] +#[wasm_bindgen] +pub fn workspaceSearch(workspace: u32, path: String, query: String) -> Promise { + future_to_promise(libparsec::WithTaskIDFuture::from(async move { + let path = { + let custom_from_rs_string = |s: String| -> Result<_, String> { + s.parse::().map_err(|e| e.to_string()) + }; + custom_from_rs_string(path).map_err(|e| TypeError::new(e.as_ref())) + }?; + + let ret = libparsec::workspace_search(workspace, path, &query).await; + Ok(match ret { + Ok(value) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &true.into())?; + let js_value = JsValue::from(value); + Reflect::set(&js_obj, &"value".into(), &js_value)?; + js_obj + } + Err(err) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &false.into())?; + let js_err = variant_workspace_search_error_rs_to_js(err)?; + Reflect::set(&js_obj, &"error".into(), &js_err)?; + js_obj + } + }) + })) +} + +// workspace_search_close +#[allow(non_snake_case)] +#[wasm_bindgen] +pub fn workspaceSearchClose(search: u32) -> Promise { + future_to_promise(libparsec::WithTaskIDFuture::from(async move { + let ret = libparsec::workspace_search_close(search); + Ok(match ret { + Ok(value) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &true.into())?; + let js_value = { + let _ = value; + JsValue::null() + }; + Reflect::set(&js_obj, &"value".into(), &js_value)?; + js_obj + } + Err(err) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &false.into())?; + let js_err = variant_workspace_search_close_error_rs_to_js(err)?; + Reflect::set(&js_obj, &"error".into(), &js_err)?; + js_obj + } + }) + })) +} + +// workspace_search_get_next +#[allow(non_snake_case)] +#[wasm_bindgen] +pub fn workspaceSearchGetNext(search: u32) -> Promise { + future_to_promise(libparsec::WithTaskIDFuture::from(async move { + let ret = libparsec::workspace_search_get_next(search).await; + Ok(match ret { + Ok(value) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &true.into())?; + let js_value = match value { + Some(val) => struct_workspace_search_match_rs_to_js(val)?, + None => JsValue::NULL, + }; + Reflect::set(&js_obj, &"value".into(), &js_value)?; + js_obj + } + Err(err) => { + let js_obj = Object::new().into(); + Reflect::set(&js_obj, &"ok".into(), &false.into())?; + let js_err = variant_workspace_search_get_next_error_rs_to_js(err)?; + Reflect::set(&js_obj, &"error".into(), &js_err)?; + js_obj + } + }) + })) +} + // workspace_stat_entry #[allow(non_snake_case)] #[wasm_bindgen] diff --git a/client/src/components/files/explorer/WorkspaceHistorySearchResultList.vue b/client/src/components/files/explorer/WorkspaceHistorySearchResultList.vue new file mode 100644 index 00000000000..4faa2513f60 --- /dev/null +++ b/client/src/components/files/explorer/WorkspaceHistorySearchResultList.vue @@ -0,0 +1,179 @@ + + + + + + + diff --git a/client/src/components/files/explorer/WorkspaceSearchResultList.vue b/client/src/components/files/explorer/WorkspaceSearchResultList.vue new file mode 100644 index 00000000000..ec70314ce5d --- /dev/null +++ b/client/src/components/files/explorer/WorkspaceSearchResultList.vue @@ -0,0 +1,187 @@ + + + + + + + diff --git a/client/src/components/files/explorer/index.ts b/client/src/components/files/explorer/index.ts index e8e1dcfce32..55e0a7a2cad 100644 --- a/client/src/components/files/explorer/index.ts +++ b/client/src/components/files/explorer/index.ts @@ -12,6 +12,8 @@ import FileListItem from '@/components/files/explorer/FileListItem.vue'; import FileListItemProcessing from '@/components/files/explorer/FileListItemProcessing.vue'; import FolderSelectionModal from '@/components/files/explorer/FolderSelectionModal.vue'; import HistoryFileListItem from '@/components/files/explorer/HistoryFileListItem.vue'; +import WorkspaceHistorySearchResultList from '@/components/files/explorer/WorkspaceHistorySearchResultList.vue'; +import WorkspaceSearchResultList from '@/components/files/explorer/WorkspaceSearchResultList.vue'; export { FileCard, @@ -26,4 +28,6 @@ export { FileListItemProcessing, FolderSelectionModal, HistoryFileListItem, + WorkspaceHistorySearchResultList, + WorkspaceSearchResultList, }; diff --git a/client/src/locales/en-US.json b/client/src/locales/en-US.json index 374a3bef0ae..1d91e7ef5ff 100644 --- a/client/src/locales/en-US.json +++ b/client/src/locales/en-US.json @@ -853,6 +853,13 @@ "actionUnselectAll": "Unselect all", "actionShare": "Share" }, + "search": { + "placeholder": "Search files...", + "searching": "Searching...", + "searchingIn": "Searching in {folder}", + "noResults": "No results found.", + "minChars": "Please enter at least 3 characters to search." + }, "importModal": { "title": "Upload your files", "dragAndDrop": "Drag and drop your files here", @@ -2637,7 +2644,13 @@ "error": "Failed to retrieve workspace history for the selected date.", "loading": "Loading...", "workspace": "Workspace:", - "date": "Preview date:" + "date": "Preview date:", + "searchMinChars": "Please enter at least 3 characters to search.", + "search": { + "placeholder": "Search files...", + "searching": "Searching...", + "noResults": "No results found." + } }, "fileViewers": { "openWithDefault": "Open with default app", diff --git a/client/src/locales/fr-FR.json b/client/src/locales/fr-FR.json index 115de3e21fd..5cda6a56952 100644 --- a/client/src/locales/fr-FR.json +++ b/client/src/locales/fr-FR.json @@ -852,6 +852,13 @@ "actionUnselectAll": "Tout désélectionner", "actionShare": "Partager" }, + "search": { + "placeholder": "Rechercher des fichiers...", + "searching": "Recherche en cours...", + "searchingIn": "Recherche dans {folder}", + "noResults": "Aucun résultat trouvé.", + "minChars": "Veuillez entrer au moins 3 caractères pour effectuer une recherche." + }, "importModal": { "title": "Importer vos fichiers", "dragAndDrop": "Glisser/Déposer vos fichiers ici", @@ -2636,7 +2643,13 @@ "error": "Impossible de récupérer l'historique de l'espace de travail pour la date sélectionnée", "loading": "Chargement...", "workspace": "Espace de travail :", - "date": "Date de prévisualisation :" + "date": "Date de prévisualisation :", + "searchMinChars": "Veuillez entrer au moins 3 caractères pour effectuer une recherche.", + "search": { + "placeholder": "Rechercher des fichiers...", + "searching": "Recherche en cours...", + "noResults": "Aucun résultat trouvé." + } }, "fileViewers": { "openWithDefault": "Ouvrir avec l'app par défaut", diff --git a/client/src/parsec/file.ts b/client/src/parsec/file.ts index d49788383d7..6a9345eb2b4 100644 --- a/client/src/parsec/file.ts +++ b/client/src/parsec/file.ts @@ -16,6 +16,7 @@ import { ParsedParsecAddrTag, ParsedParsecAddrWorkspacePath, Result, + SearchHandle, WorkspaceCreateFileError, WorkspaceCreateFolderError, WorkspaceFdCloseError, @@ -26,6 +27,10 @@ import { WorkspaceMoveEntryError, WorkspaceOpenFileError, WorkspaceRemoveEntryError, + WorkspaceSearchCloseError, + WorkspaceSearchError, + WorkspaceSearchGetNextError, + WorkspaceSearchMatch, WorkspaceStatEntryError, WorkspaceStatFolderChildrenError, } from '@/parsec/types'; @@ -334,3 +339,21 @@ export async function isFileContentAvailable(workspaceHandle: WorkspaceHandle, p return result.ok && result.value; } + +export async function startWorkspaceSearch( + workspaceHandle: WorkspaceHandle, + path: FsPath, + query: string, +): Promise> { + return await libparsec.workspaceSearch(workspaceHandle, path, query); +} + +export async function getNextWorkspaceSearchResult( + searchHandle: SearchHandle, +): Promise> { + return await libparsec.workspaceSearchGetNext(searchHandle); +} + +export async function closeWorkspaceSearch(searchHandle: SearchHandle): Promise> { + return await libparsec.workspaceSearchClose(searchHandle); +} diff --git a/client/src/parsec/history.ts b/client/src/parsec/history.ts index 4733e5ad65b..c0078b34c5c 100644 --- a/client/src/parsec/history.ts +++ b/client/src/parsec/history.ts @@ -5,6 +5,7 @@ import { FileDescriptor, FsPath, Result, + SearchHandle, WorkspaceHistoryEntryStat, WorkspaceHistoryEntryStatFile, WorkspaceHistoryEntryStatFolder, @@ -18,6 +19,11 @@ import { WorkspaceHistoryInternalOnlyErrorTag, WorkspaceHistoryOpenFileError, WorkspaceHistoryOpenFileErrorTag, + WorkspaceHistorySearchCloseError, + WorkspaceHistorySearchError, + WorkspaceHistorySearchErrorTag, + WorkspaceHistorySearchGetNextError, + WorkspaceHistorySearchMatch, WorkspaceHistorySetTimestampOfInterestError, WorkspaceHistorySetTimestampOfInterestErrorTag, WorkspaceHistoryStartError, @@ -198,6 +204,21 @@ export class WorkspaceHistory { } return await libparsec.workspaceHistoryFdRead(this.handle, fd, BigInt(offset), BigInt(size)); } + + async search(path: FsPath, query: string): Promise> { + if (this.handle === undefined) { + return { ok: false, error: { tag: WorkspaceHistorySearchErrorTag.Internal, error: 'Not started' } }; + } + return await libparsec.workspaceHistorySearch(this.handle, path, query); + } + + async searchGetNext(searchHandle: SearchHandle): Promise> { + return await libparsec.workspaceHistorySearchGetNext(searchHandle); + } + + async searchClose(searchHandle: SearchHandle): Promise> { + return await libparsec.workspaceHistorySearchClose(searchHandle); + } } export interface HistoryEntryTree { diff --git a/client/src/parsec/types.ts b/client/src/parsec/types.ts index 34fd69d103f..027992ed7dc 100644 --- a/client/src/parsec/types.ts +++ b/client/src/parsec/types.ts @@ -88,6 +88,9 @@ export { WorkspaceHistoryFdReadErrorTag, WorkspaceHistoryInternalOnlyErrorTag, WorkspaceHistoryOpenFileErrorTag, + WorkspaceHistorySearchCloseErrorTag, + WorkspaceHistorySearchErrorTag, + WorkspaceHistorySearchGetNextErrorTag, WorkspaceHistorySetTimestampOfInterestErrorTag, WorkspaceHistoryStatEntryErrorTag, WorkspaceHistoryStatFolderChildrenErrorTag, @@ -97,6 +100,9 @@ export { WorkspaceMoveEntryErrorTag, WorkspaceOpenFileErrorTag, WorkspaceRemoveEntryErrorTag, + WorkspaceSearchCloseErrorTag, + WorkspaceSearchErrorTag, + WorkspaceSearchGetNextErrorTag, WorkspaceStatEntryErrorTag, WorkspaceStatFolderChildrenErrorTag, WorkspaceStopErrorTag, @@ -259,6 +265,10 @@ export type { WorkspaceHistoryFdReadError, WorkspaceHistoryInternalOnlyError, WorkspaceHistoryOpenFileError, + WorkspaceHistorySearchCloseError, + WorkspaceHistorySearchError, + WorkspaceHistorySearchGetNextError, + WorkspaceHistorySearchMatch, WorkspaceHistorySetTimestampOfInterestError, WorkspaceHistoryStartError, WorkspaceHistoryStatEntryError, @@ -270,6 +280,10 @@ export type { WorkspaceMoveEntryError, WorkspaceOpenFileError, WorkspaceRemoveEntryError, + WorkspaceSearchCloseError, + WorkspaceSearchError, + WorkspaceSearchGetNextError, + WorkspaceSearchMatch, WorkspaceStatEntryError, WorkspaceStatFolderChildrenError, WorkspaceStopError, @@ -307,6 +321,7 @@ import { InvitationType, RealmRole as WorkspaceRole } from '@/plugins/libparsec' type WorkspaceHistoryHandle = Handle; type WorkspaceHandle = Handle; +type SearchHandle = Handle; type EntryID = VlobID; type WorkspaceName = EntryName; type ConnectionHandle = Handle; @@ -490,6 +505,7 @@ export { OwnDeviceInfo, ParsecOrganizationAddr, RegistrationDevice, + SearchHandle, StartedWorkspaceInfo, SystemPath, UserID, diff --git a/client/src/plugins/libparsec/definitions.ts b/client/src/plugins/libparsec/definitions.ts index 07bf60e2498..3a03632257f 100644 --- a/client/src/plugins/libparsec/definitions.ts +++ b/client/src/plugins/libparsec/definitions.ts @@ -530,6 +530,13 @@ export interface WorkspaceHistoryFileStat { size: SizeInt } +export interface WorkspaceHistorySearchMatch { + path: FsPath + stat: WorkspaceHistoryEntryStat + score: U32 + matchPositions: Array +} + export interface WorkspaceInfo { id: VlobID currentName: EntryName @@ -538,6 +545,13 @@ export interface WorkspaceInfo { isBootstrapped: boolean } +export interface WorkspaceSearchMatch { + path: FsPath + stat: EntryStat + score: U32 + matchPositions: Array +} + export interface WorkspaceUserAccessInfo { userId: UserID humanHandle: HumanHandle @@ -5185,6 +5199,42 @@ export type WorkspaceHistoryRealmExportDecryptor = | WorkspaceHistoryRealmExportDecryptorSequesterService | WorkspaceHistoryRealmExportDecryptorUser +// WorkspaceHistorySearchCloseError +export enum WorkspaceHistorySearchCloseErrorTag { + Internal = 'WorkspaceHistorySearchCloseErrorInternal', +} + +export interface WorkspaceHistorySearchCloseErrorInternal { + tag: WorkspaceHistorySearchCloseErrorTag.Internal + error: string +} +export type WorkspaceHistorySearchCloseError = + | WorkspaceHistorySearchCloseErrorInternal + +// WorkspaceHistorySearchError +export enum WorkspaceHistorySearchErrorTag { + Internal = 'WorkspaceHistorySearchErrorInternal', +} + +export interface WorkspaceHistorySearchErrorInternal { + tag: WorkspaceHistorySearchErrorTag.Internal + error: string +} +export type WorkspaceHistorySearchError = + | WorkspaceHistorySearchErrorInternal + +// WorkspaceHistorySearchGetNextError +export enum WorkspaceHistorySearchGetNextErrorTag { + Internal = 'WorkspaceHistorySearchGetNextErrorInternal', +} + +export interface WorkspaceHistorySearchGetNextErrorInternal { + tag: WorkspaceHistorySearchGetNextErrorTag.Internal + error: string +} +export type WorkspaceHistorySearchGetNextError = + | WorkspaceHistorySearchGetNextErrorInternal + // WorkspaceHistorySetTimestampOfInterestError export enum WorkspaceHistorySetTimestampOfInterestErrorTag { EntryNotFound = 'WorkspaceHistorySetTimestampOfInterestErrorEntryNotFound', @@ -5785,6 +5835,42 @@ export type WorkspaceRemoveEntryError = | WorkspaceRemoveEntryErrorReadOnlyRealm | WorkspaceRemoveEntryErrorStopped +// WorkspaceSearchCloseError +export enum WorkspaceSearchCloseErrorTag { + Internal = 'WorkspaceSearchCloseErrorInternal', +} + +export interface WorkspaceSearchCloseErrorInternal { + tag: WorkspaceSearchCloseErrorTag.Internal + error: string +} +export type WorkspaceSearchCloseError = + | WorkspaceSearchCloseErrorInternal + +// WorkspaceSearchError +export enum WorkspaceSearchErrorTag { + Internal = 'WorkspaceSearchErrorInternal', +} + +export interface WorkspaceSearchErrorInternal { + tag: WorkspaceSearchErrorTag.Internal + error: string +} +export type WorkspaceSearchError = + | WorkspaceSearchErrorInternal + +// WorkspaceSearchGetNextError +export enum WorkspaceSearchGetNextErrorTag { + Internal = 'WorkspaceSearchGetNextErrorInternal', +} + +export interface WorkspaceSearchGetNextErrorInternal { + tag: WorkspaceSearchGetNextErrorTag.Internal + error: string +} +export type WorkspaceSearchGetNextError = + | WorkspaceSearchGetNextErrorInternal + // WorkspaceStatEntryError export enum WorkspaceStatEntryErrorTag { EntryNotFound = 'WorkspaceStatEntryErrorEntryNotFound', @@ -6709,6 +6795,17 @@ export interface LibParsecPlugin { workspace_history: Handle, entry_id: VlobID ): Promise> + workspaceHistorySearch( + workspace_history: Handle, + path: FsPath, + query: string + ): Promise> + workspaceHistorySearchClose( + search: Handle + ): Promise> + workspaceHistorySearchGetNext( + search: Handle + ): Promise> workspaceHistorySetTimestampOfInterest( workspace_history: Handle, toi: DateTime @@ -6791,6 +6888,17 @@ export interface LibParsecPlugin { dst_name: EntryName, mode: MoveEntryMode ): Promise> + workspaceSearch( + workspace: Handle, + path: FsPath, + query: string + ): Promise> + workspaceSearchClose( + search: Handle + ): Promise> + workspaceSearchGetNext( + search: Handle + ): Promise> workspaceStatEntry( workspace: Handle, path: FsPath diff --git a/client/src/views/files/FoldersPage.vue b/client/src/views/files/FoldersPage.vue index 3510be563f3..d455778037d 100644 --- a/client/src/views/files/FoldersPage.vue +++ b/client/src/views/files/FoldersPage.vue @@ -11,6 +11,13 @@ v-if="isLargeDisplay" :buttons="actionBarOptionsFoldersPage" > +
@@ -104,7 +111,22 @@
+ {{ $msTranslate('FoldersPage.search.minChars') }} +
+ + + +
@@ -204,6 +226,7 @@ import { MsOptions, MsReportText, MsReportTheme, + MsSearchInput, MsSorter, MsSorterChangeEvent, MsSpinner, @@ -230,6 +253,7 @@ import { FoldersPageSavedData, ImportType, SortProperty, + WorkspaceSearchResultList, copyPathLinkToClipboard, selectFolder, } from '@/components/files'; @@ -241,16 +265,22 @@ import { ClientInfo, EntryName, EntryStatFile, + FileType, FsPath, Path, + SearchHandle, WorkspaceCreateFolderErrorTag, WorkspaceID, WorkspaceRole, + WorkspaceSearchMatch, + closeWorkspaceSearch, entryStat, getClientInfo, + getNextWorkspaceSearchResult, isDesktop, isWeb, listWorkspaces, + startWorkspaceSearch, } from '@/parsec'; import { Routes, currentRouteIs, getCurrentRouteQuery, getDocumentPath, getWorkspaceHandle, navigateTo, watchRoute } from '@/router'; import { isFileEditable } from '@/services/cryptpad'; @@ -447,6 +477,20 @@ const fileGridDisplayRef = useTemplateRef>( const fileInputsRef = useTemplateRef>('fileInputs'); let eventCbId: string | null = null; +const searchQuery = ref(''); +const searchResults = ref([]); +const isSearching = ref(false); +let currentSearchHandle: SearchHandle | null = null; + +watch(searchQuery, (newQuery) => { + searchResults.value = []; + if (newQuery) { + isSearching.value = true; + } else { + isSearching.value = false; + } +}); + const selectedFilesCount = computed(() => { return files.value.selectedCount() + folders.value.selectedCount(); }); @@ -709,6 +753,7 @@ onUnmounted(async () => { if (eventCbId) { eventDistributor.value.removeCallback(eventCbId); } + await cancelCurrentSearch(); }); async function updateWorkspaceInfo(workspaceId: WorkspaceID): Promise { @@ -1514,6 +1559,72 @@ async function onDropAsReader(): Promise { ); } +async function cancelCurrentSearch(): Promise { + if (currentSearchHandle !== null) { + const handle = currentSearchHandle; + currentSearchHandle = null; + await closeWorkspaceSearch(handle); + } +} + +async function onSearchChange(query: string): Promise { + await cancelCurrentSearch(); + searchResults.value = []; + + if (!query || !workspaceInfo.value) { + isSearching.value = false; + return; + } + + isSearching.value = true; + const startResult = await startWorkspaceSearch(workspaceInfo.value.handle, currentPath.value, query); + if (!startResult.ok) { + isSearching.value = false; + return; + } + + const handle = startResult.value; + currentSearchHandle = handle; + + while (currentSearchHandle === handle) { + const nextResult = await getNextWorkspaceSearchResult(handle); + if (!nextResult.ok || nextResult.value === null) { + break; + } + if (currentSearchHandle === handle) { + searchResults.value.push(nextResult.value); + } + } + + if (currentSearchHandle === handle) { + isSearching.value = false; + currentSearchHandle = null; + await closeWorkspaceSearch(handle); + } +} + +async function onSearchResultClick(match: WorkspaceSearchMatch): Promise { + if (!workspaceInfo.value) { + return; + } + await cancelCurrentSearch(); + searchQuery.value = ''; + searchResults.value = []; + + const isFile = match.stat.tag === FileType.File; + if (isFile) { + const parentPath = await parsec.Path.parent(match.path); + const filename = await parsec.Path.filename(match.path); + navigateTo(Routes.Documents, { + query: { documentPath: parentPath, workspaceHandle: workspaceInfo.value.handle, selectFile: filename ?? undefined }, + }); + } else { + navigateTo(Routes.Documents, { + query: { documentPath: match.path, workspaceHandle: workspaceInfo.value.handle }, + }); + } +} + const actionBarOptionsFoldersPage = computed(() => { const actionArray = []; const selectedEntries = getSelectedEntries(); @@ -1714,6 +1825,17 @@ const actionBarOptionsFoldersPage = computed(() => { } } +.search-input { + flex-shrink: 1; + min-width: 10rem; + max-width: 20rem; +} + +.search-result-list { + flex-grow: 1; + overflow-y: auto; +} + .workspace-role-tag { background: var(--parsec-color-light-secondary-white); padding: 0.25rem; diff --git a/client/src/views/workspaces/WorkspaceHistoryPage.vue b/client/src/views/workspaces/WorkspaceHistoryPage.vue index 5ec880deb6e..7eb740843f8 100644 --- a/client/src/views/workspaces/WorkspaceHistoryPage.vue +++ b/client/src/views/workspaces/WorkspaceHistoryPage.vue @@ -86,22 +86,25 @@ class="folder-header__actions" ref="topbarRight" > + {{ $msTranslate('workspaceHistory.actions.selectAll') }} {{ $msTranslate('workspaceHistory.actions.deselectAll') }} -
{{ $msTranslate('workspaceHistory.loading') }}
{{ $msTranslate('workspaceHistory.empty') }}
{{ $msTranslate(error) }}
+
@@ -180,10 +191,25 @@