From 713903fe76086551bdb6cc701ef81858c6964ee8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 4 Nov 2025 00:01:52 +0200 Subject: [PATCH] Fix incorrect search ranges when rendering search matches in the outline panel (#41859) Closes https://github.com/zed-industries/zed/issues/41792 Release Notes: - Fixed outline panel panicking when rendering certain search matches --- crates/outline_panel/src/outline_panel.rs | 573 ++++++++++++++++++---- 1 file changed, 484 insertions(+), 89 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 112aa3d21ebda9ef57d3bedda20e3f90735a0173..a9a8d217a229b564a3e7d8b6963334c08ff718f3 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -465,14 +465,8 @@ impl SearchData { let match_offset_range = match_range.to_offset(multi_buffer_snapshot); let mut search_match_indices = vec![ - multi_buffer_snapshot.clip_offset( - match_offset_range.start - context_offset_range.start, - Bias::Left, - ) - ..multi_buffer_snapshot.clip_offset( - match_offset_range.end - context_offset_range.start, - Bias::Right, - ), + match_offset_range.start - context_offset_range.start + ..match_offset_range.end - context_offset_range.start, ]; let entire_context_text = multi_buffer_snapshot @@ -509,14 +503,8 @@ impl SearchData { .next() .is_some_and(|c| !c.is_whitespace()); search_match_indices.iter_mut().for_each(|range| { - range.start = multi_buffer_snapshot.clip_offset( - range.start.saturating_sub(left_whitespaces_offset), - Bias::Left, - ); - range.end = multi_buffer_snapshot.clip_offset( - range.end.saturating_sub(left_whitespaces_offset), - Bias::Right, - ); + range.start = range.start.saturating_sub(left_whitespaces_offset); + range.end = range.end.saturating_sub(left_whitespaces_offset); }); let trimmed_row_offset_range = @@ -5226,10 +5214,13 @@ mod tests { use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust}; use pretty_assertions::assert_eq; use project::FakeFs; - use search::project_search::{self, perform_project_search}; + use search::{ + buffer_search, + project_search::{self, perform_project_search}, + }; use serde_json::json; use util::path; - use workspace::{OpenOptions, OpenVisible}; + use workspace::{OpenOptions, OpenVisible, ToolbarItemView}; use super::*; @@ -5292,25 +5283,28 @@ mod tests { ide/src/ inlay_hints/ fn_lifetime_fn.rs - search: match config.param_names_for_lifetime_elision_hints { - search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - search: Some(it) if config.param_names_for_lifetime_elision_hints => { - search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + search: match config.«param_names_for_lifetime_elision_hints» { + search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» { + search: Some(it) if config.«param_names_for_lifetime_elision_hints» => { + search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG }, inlay_hints.rs - search: pub param_names_for_lifetime_elision_hints: bool, - search: param_names_for_lifetime_elision_hints: self + search: pub «param_names_for_lifetime_elision_hints»: bool, + search: «param_names_for_lifetime_elision_hints»: self static_index.rs - search: param_names_for_lifetime_elision_hints: false, + search: «param_names_for_lifetime_elision_hints»: false, rust-analyzer/src/ cli/ analysis_stats.rs - search: param_names_for_lifetime_elision_hints: true, + search: «param_names_for_lifetime_elision_hints»: true, config.rs - search: param_names_for_lifetime_elision_hints: self"# + search: «param_names_for_lifetime_elision_hints»: self"# .to_string(); let select_first_in_all_matches = |line_to_select: &str| { - assert!(all_matches.contains(line_to_select)); + assert!( + all_matches.contains(line_to_select), + "`{line_to_select}` was not found in all matches `{all_matches}`" + ); all_matches.replacen( line_to_select, &format!("{line_to_select}{SELECTED_MARKER}"), @@ -5331,7 +5325,7 @@ mod tests { cx, ), select_first_in_all_matches( - "search: match config.param_names_for_lifetime_elision_hints {" + "search: match config.«param_names_for_lifetime_elision_hints» {" ) ); }); @@ -5371,16 +5365,16 @@ mod tests { inlay_hints/ fn_lifetime_fn.rs{SELECTED_MARKER} inlay_hints.rs - search: pub param_names_for_lifetime_elision_hints: bool, - search: param_names_for_lifetime_elision_hints: self + search: pub «param_names_for_lifetime_elision_hints»: bool, + search: «param_names_for_lifetime_elision_hints»: self static_index.rs - search: param_names_for_lifetime_elision_hints: false, + search: «param_names_for_lifetime_elision_hints»: false, rust-analyzer/src/ cli/ analysis_stats.rs - search: param_names_for_lifetime_elision_hints: true, + search: «param_names_for_lifetime_elision_hints»: true, config.rs - search: param_names_for_lifetime_elision_hints: self"#, + search: «param_names_for_lifetime_elision_hints»: self"#, ) ); }); @@ -5441,9 +5435,9 @@ mod tests { rust-analyzer/src/ cli/ analysis_stats.rs - search: param_names_for_lifetime_elision_hints: true, + search: «param_names_for_lifetime_elision_hints»: true, config.rs - search: param_names_for_lifetime_elision_hints: self"#, + search: «param_names_for_lifetime_elision_hints»: self"#, ) ); }); @@ -5523,21 +5517,21 @@ mod tests { ide/src/ inlay_hints/ fn_lifetime_fn.rs - search: match config.param_names_for_lifetime_elision_hints { - search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - search: Some(it) if config.param_names_for_lifetime_elision_hints => { - search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + search: match config.«param_names_for_lifetime_elision_hints» { + search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» { + search: Some(it) if config.«param_names_for_lifetime_elision_hints» => { + search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG }, inlay_hints.rs - search: pub param_names_for_lifetime_elision_hints: bool, - search: param_names_for_lifetime_elision_hints: self + search: pub «param_names_for_lifetime_elision_hints»: bool, + search: «param_names_for_lifetime_elision_hints»: self static_index.rs - search: param_names_for_lifetime_elision_hints: false, + search: «param_names_for_lifetime_elision_hints»: false, rust-analyzer/src/ cli/ analysis_stats.rs - search: param_names_for_lifetime_elision_hints: true, + search: «param_names_for_lifetime_elision_hints»: true, config.rs - search: param_names_for_lifetime_elision_hints: self"# + search: «param_names_for_lifetime_elision_hints»: self"# .to_string(); cx.executor() @@ -5662,30 +5656,40 @@ mod tests { ide/src/ inlay_hints/ fn_lifetime_fn.rs - search: match config.param_names_for_lifetime_elision_hints { - search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - search: Some(it) if config.param_names_for_lifetime_elision_hints => { - search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + search: match config.«param_names_for_lifetime_elision_hints» { + search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» { + search: Some(it) if config.«param_names_for_lifetime_elision_hints» => { + search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG }, inlay_hints.rs - search: pub param_names_for_lifetime_elision_hints: bool, - search: param_names_for_lifetime_elision_hints: self + search: pub «param_names_for_lifetime_elision_hints»: bool, + search: «param_names_for_lifetime_elision_hints»: self static_index.rs - search: param_names_for_lifetime_elision_hints: false, + search: «param_names_for_lifetime_elision_hints»: false, rust-analyzer/src/ cli/ analysis_stats.rs - search: param_names_for_lifetime_elision_hints: true, + search: «param_names_for_lifetime_elision_hints»: true, config.rs - search: param_names_for_lifetime_elision_hints: self"# + search: «param_names_for_lifetime_elision_hints»: self"# .to_string(); let select_first_in_all_matches = |line_to_select: &str| { - assert!(all_matches.contains(line_to_select)); + assert!( + all_matches.contains(line_to_select), + "`{line_to_select}` was not found in all matches `{all_matches}`" + ); all_matches.replacen( line_to_select, &format!("{line_to_select}{SELECTED_MARKER}"), 1, ) }; + let clear_outline_metadata = |input: &str| { + input + .replace("search: ", "") + .replace("«", "") + .replace("»", "") + }; + cx.executor() .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); cx.run_until_parked(); @@ -5696,7 +5700,7 @@ mod tests { .expect("should have an active editor open") }); let initial_outline_selection = - "search: match config.param_names_for_lifetime_elision_hints {"; + "search: match config.«param_names_for_lifetime_elision_hints» {"; outline_panel.update_in(cx, |outline_panel, window, cx| { assert_eq!( display_entries( @@ -5710,7 +5714,7 @@ mod tests { ); assert_eq!( selected_row_text(&active_editor, cx), - initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes + clear_outline_metadata(initial_outline_selection), "Should place the initial editor selection on the corresponding search result" ); @@ -5719,7 +5723,7 @@ mod tests { }); let navigated_outline_selection = - "search: Some(it) if config.param_names_for_lifetime_elision_hints => {"; + "search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {"; outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( @@ -5737,7 +5741,7 @@ mod tests { outline_panel.update(cx, |_, cx| { assert_eq!( selected_row_text(&active_editor, cx), - navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes + clear_outline_metadata(navigated_outline_selection), "Should still have the initial caret position after SelectNext calls" ); }); @@ -5748,7 +5752,7 @@ mod tests { outline_panel.update(cx, |_outline_panel, cx| { assert_eq!( selected_row_text(&active_editor, cx), - navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes + clear_outline_metadata(navigated_outline_selection), "After opening, should move the caret to the opened outline entry's position" ); }); @@ -5756,7 +5760,7 @@ mod tests { outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.select_next(&SelectNext, window, cx); }); - let next_navigated_outline_selection = "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },"; + let next_navigated_outline_selection = "search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },"; outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( @@ -5774,7 +5778,7 @@ mod tests { outline_panel.update(cx, |_outline_panel, cx| { assert_eq!( selected_row_text(&active_editor, cx), - next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes + clear_outline_metadata(next_navigated_outline_selection), "Should again preserve the selection after another SelectNext call" ); }); @@ -5807,7 +5811,7 @@ mod tests { ); assert_eq!( selected_row_text(&new_active_editor, cx), - next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes + clear_outline_metadata(next_navigated_outline_selection), "When opening the excerpt, should navigate to the place corresponding the outline entry" ); }); @@ -5909,11 +5913,11 @@ mod tests { format!( r#"one/ a.txt - search: aaa aaa <==== selected - search: aaa aaa + search: «aaa» aaa <==== selected + search: aaa «aaa» two/ b.txt - search: a aaa"#, + search: a «aaa»"#, ), ); }); @@ -5939,7 +5943,7 @@ two/ a.txt <==== selected two/ b.txt - search: a aaa"#, + search: a «aaa»"#, ), ); }); @@ -5988,7 +5992,7 @@ two/ <==== selected"#, a.txt two/ <==== selected b.txt - search: a aaa"#, + search: a «aaa»"#, ) ); }); @@ -6453,18 +6457,18 @@ outline: struct OutlineEntryExcerpt r#"frontend-project/ public/lottie/ syntax-tree.json - search: {{ "something": "static" }} <==== selected + search: {{ "something": "«static»" }} <==== selected src/ app/(site)/ (about)/jobs/[slug]/ page.tsx - search: static + search: «static» (blog)/post/[slug]/ page.tsx - search: static + search: «static» components/ ErrorBoundary.tsx - search: static"# + search: «static»"# ) ); }); @@ -6492,12 +6496,12 @@ outline: struct OutlineEntryExcerpt r#"frontend-project/ public/lottie/ syntax-tree.json - search: {{ "something": "static" }} + search: {{ "something": "«static»" }} src/ app/(site)/ <==== selected components/ ErrorBoundary.tsx - search: static"# + search: «static»"# ) ); }); @@ -6522,12 +6526,12 @@ outline: struct OutlineEntryExcerpt r#"frontend-project/ public/lottie/ syntax-tree.json - search: {{ "something": "static" }} + search: {{ "something": "«static»" }} src/ app/(site)/ components/ ErrorBoundary.tsx - search: static <==== selected"# + search: «static» <==== selected"# ) ); }); @@ -6556,7 +6560,7 @@ outline: struct OutlineEntryExcerpt r#"frontend-project/ public/lottie/ syntax-tree.json - search: {{ "something": "static" }} + search: {{ "something": "«static»" }} src/ app/(site)/ components/ @@ -6589,12 +6593,66 @@ outline: struct OutlineEntryExcerpt r#"frontend-project/ public/lottie/ syntax-tree.json - search: {{ "something": "static" }} + search: {{ "something": "«static»" }} + src/ + app/(site)/ + components/ + ErrorBoundary.tsx <==== selected + search: «static»"# + ) + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.collapse_all_entries(&CollapseAllEntries, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + format!(r#"frontend-project/"#) + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.expand_all_entries(&ExpandAllEntries, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + format!( + r#"frontend-project/ + public/lottie/ + syntax-tree.json + search: {{ "something": "«static»" }} src/ app/(site)/ + (about)/jobs/[slug]/ + page.tsx + search: «static» + (blog)/post/[slug]/ + page.tsx + search: «static» components/ ErrorBoundary.tsx <==== selected - search: static"# + search: «static»"# ) ); }); @@ -6700,16 +6758,21 @@ outline: struct OutlineEntryExcerpt } }, PanelEntry::Search(search_entry) => { - format!( - "search: {}", - search_entry - .render_data - .get_or_init(|| SearchData::new( - &search_entry.match_range, - multi_buffer_snapshot - )) - .context_text - ) + let search_data = search_entry.render_data.get_or_init(|| { + SearchData::new(&search_entry.match_range, multi_buffer_snapshot) + }); + let mut search_result = String::new(); + let mut last_end = 0; + for range in &search_data.search_match_indices { + search_result.push_str(&search_data.context_text[last_end..range.start]); + search_result.push('«'); + search_result.push_str(&search_data.context_text[range.start..range.end]); + search_result.push('»'); + last_end = range.end; + } + search_result.push_str(&search_data.context_text[last_end..]); + + format!("search: {search_result}") } }; @@ -6732,6 +6795,7 @@ outline: struct OutlineEntryExcerpt workspace::init_settings(cx); Project::init_settings(cx); project_search::init(cx); + buffer_search::init(cx); super::init(cx); }); } @@ -7510,4 +7574,335 @@ outline: fn main()" ); }); } + + #[gpui::test] + async fn test_outline_expand_collapse_all(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/test", + json!({ + "src": { + "lib.rs": indoc!(" + mod outer { + pub struct OuterStruct { + field: String, + } + impl OuterStruct { + pub fn new() -> Self { + Self { field: String::new() } + } + pub fn method(&self) { + println!(\"{}\", self.field); + } + } + mod inner { + pub fn inner_function() { + let x = 42; + println!(\"{}\", x); + } + pub struct InnerStruct { + value: i32, + } + } + } + fn main() { + let s = outer::OuterStruct::new(); + s.method(); + } + "), + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + project.read_with(cx, |project, _| { + project.languages().add(Arc::new( + rust_lang() + .with_outline_query( + r#" + (struct_item + (visibility_modifier)? @context + "struct" @context + name: (_) @name) @item + (impl_item + "impl" @context + trait: (_)? @context + "for"? @context + type: (_) @context + body: (_)) @item + (function_item + (visibility_modifier)? @context + "fn" @context + name: (_) @name + parameters: (_) @context) @item + (mod_item + (visibility_modifier)? @context + "mod" @context + name: (_) @name) @item + (enum_item + (visibility_modifier)? @context + "enum" @context + name: (_) @name) @item + (field_declaration + (visibility_modifier)? @context + name: (_) @name + ":" @context + type: (_) @context) @item + "#, + ) + .unwrap(), + )) + }); + let workspace = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let outline_panel = outline_panel(&workspace, cx); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.set_active(true, window, cx) + }); + + workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from("/test/src/lib.rs"), + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + window, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); + cx.run_until_parked(); + + // Force another update cycle to ensure outlines are fetched + outline_panel.update_in(cx, |panel, window, cx| { + panel.update_non_fs_items(window, cx); + panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: mod outer <==== selected + outline: pub struct OuterStruct + outline: field: String + outline: impl OuterStruct + outline: pub fn new() + outline: pub fn method(&self) + outline: mod inner + outline: pub fn inner_function() + outline: pub struct InnerStruct + outline: value: i32 +outline: fn main()" + ) + ); + }); + + let _parent_outline = outline_panel + .read_with(cx, |panel, _cx| { + panel + .cached_entries + .iter() + .find_map(|entry| match &entry.entry { + PanelEntry::Outline(OutlineEntry::Outline(outline)) + if panel + .outline_children_cache + .get(&outline.buffer_id) + .and_then(|children_map| { + let key = + (outline.outline.range.clone(), outline.outline.depth); + children_map.get(&key) + }) + .copied() + .unwrap_or(false) => + { + Some(entry.entry.clone()) + } + _ => None, + }) + }) + .expect("Should find an outline with children"); + + // Collapse all entries + outline_panel.update_in(cx, |panel, window, cx| { + panel.collapse_all_entries(&CollapseAllEntries, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + let expected_collapsed_output = indoc!( + " + outline: mod outer <==== selected + outline: fn main()" + ); + + outline_panel.update(cx, |panel, cx| { + assert_eq! { + display_entries( + &project, + &snapshot(panel, cx), + &panel.cached_entries, + panel.selected_entry(), + cx, + ), + expected_collapsed_output + }; + }); + + // Expand all entries + outline_panel.update_in(cx, |panel, window, cx| { + panel.expand_all_entries(&ExpandAllEntries, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + let expected_expanded_output = indoc!( + " + outline: mod outer <==== selected + outline: pub struct OuterStruct + outline: field: String + outline: impl OuterStruct + outline: pub fn new() + outline: pub fn method(&self) + outline: mod inner + outline: pub fn inner_function() + outline: pub struct InnerStruct + outline: value: i32 + outline: fn main()" + ); + + outline_panel.update(cx, |panel, cx| { + assert_eq! { + display_entries( + &project, + &snapshot(panel, cx), + &panel.cached_entries, + panel.selected_entry(), + cx, + ), + expected_expanded_output + }; + }); + } + + #[gpui::test] + async fn test_buffer_search(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/test", + json!({ + "foo.txt": r#"<_constitution> + + + + + +## 📊 Output + +| Field | Meaning | +"# + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + let workspace = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let editor = workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from("/test/foo.txt"), + OpenOptions { + visible: Some(OpenVisible::All), + ..OpenOptions::default() + }, + window, + cx, + ) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + let search_bar = workspace + .update(cx, |_, window, cx| { + cx.new(|cx| { + let mut search_bar = BufferSearchBar::new(None, window, cx); + search_bar.set_active_pane_item(Some(&editor), window, cx); + search_bar.show(window, cx); + search_bar + }) + }) + .unwrap(); + + let outline_panel = outline_panel(&workspace, cx); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.set_active(true, window, cx) + }); + + search_bar + .update_in(cx, |search_bar, window, cx| { + search_bar.search(" ", None, true, window, cx) + }) + .await + .unwrap(); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + "search: | Field« » | Meaning | <==== selected +search: | Field « » | Meaning | +search: | Field « » | Meaning | +search: | Field « » | Meaning | +search: | Field « »| Meaning | +search: | Field | Meaning« » | +search: | Field | Meaning « » | +search: | Field | Meaning « » | +search: | Field | Meaning « » | +search: | Field | Meaning « » | +search: | Field | Meaning « » | +search: | Field | Meaning « » | +search: | Field | Meaning « »|" + ); + }); + } }