From 50c3395e318ae9872db034e277a3edcd6eabd6ce Mon Sep 17 00:00:00 2001 From: rsdy Date: Tue, 7 Nov 2023 17:20:16 +0100 Subject: [PATCH 01/42] Respect path filters in agent queries --- server/bleep/src/agent.rs | 42 ++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/server/bleep/src/agent.rs b/server/bleep/src/agent.rs index d07c6304f0..fdab2f21a4 100644 --- a/server/bleep/src/agent.rs +++ b/server/bleep/src/agent.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{collections::HashSet, sync::Arc, time::Duration}; use anyhow::{anyhow, Context, Result}; use futures::{Future, TryStreamExt}; @@ -372,13 +372,45 @@ impl Agent { threshold: f32, retrieve_more: bool, ) -> Result> { + let paths_set = paths + .into_iter() + .map(|p| parser::Literal::Plain(p.into())) + .collect::>(); + + let paths = if paths_set.is_empty() { + self.last_exchange().query.paths.clone() + } else if self.last_exchange().query.paths.is_empty() { + paths_set + } else { + paths_set + .into_iter() + .zip(self.last_exchange().query.paths.clone()) + .flat_map(|(llm, user)| { + if llm + .as_plain() + .unwrap() + .starts_with(user.as_plain().unwrap().as_ref()) + { + // llm-defined is more specific than user request + vec![llm] + } else if user + .as_plain() + .unwrap() + .starts_with(llm.as_plain().unwrap().as_ref()) + { + // user-defined is more specific than llm request + vec![user] + } else { + vec![llm, user] + } + }) + .collect() + }; + let query = parser::SemanticQuery { target: Some(query), repos: [parser::Literal::Plain(self.repo_ref.display_name().into())].into(), - paths: paths - .iter() - .map(|p| parser::Literal::Plain(p.into())) - .collect(), + paths, ..self.last_exchange().query.clone() }; From b6425f5fdcf1afe47ea129b8a6d509dcb43887d2 Mon Sep 17 00:00:00 2001 From: rsdy Date: Tue, 7 Nov 2023 19:32:54 +0100 Subject: [PATCH 02/42] Fix autocomplete for langs --- server/bleep/src/webserver/autocomplete.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 16561a3eb4..d2abcc21d5 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use super::prelude::*; use crate::{ indexes::{ - reader::{ContentReader, FileReader, RepoReader}, + reader::{ContentReader, FileReader, OpenReader, RepoReader}, Indexes, }, query::{ @@ -41,13 +41,6 @@ pub(super) async fn handle( ); } - // Bypass the parser and execute a prefix search using the last whitespace-split token - // in the query string. - // - // This should be revisited when we implement cursor-aware autocomplete. - // - // `api lang:p` -> search lang list with prefix `p` - // `lang:p api` -> lang prefix search not triggered if let Some(matched_langs) = complete_lang(&api_params.q) { autocomplete_results.append( &mut matched_langs @@ -88,12 +81,16 @@ fn complete_flag(q: &str) -> impl Iterator + '_ { .copied() } +// Bypass the parser and execute a prefix search using the rightmost whitespace-split token +// in the query string. +// +// This should be revisited when we implement cursor-aware autocomplete. fn complete_lang(q: &str) -> Option + '_> { - match q.split_whitespace().last() { + match q.split_whitespace().rfind(|comp| comp.starts_with("lang:")) { Some(last) => last.strip_prefix("lang:").map(|prefix| { COMMON_LANGUAGES .iter() - .filter(move |l| l.starts_with(prefix)) + .filter(move |l| l.starts_with(prefix) && **l != prefix) .copied() }), _ => None, From db5ff0d973793339c0c85dd2adb37203a5d00b44 Mon Sep 17 00:00:00 2001 From: rsdy Date: Thu, 9 Nov 2023 17:35:14 +0100 Subject: [PATCH 03/42] Add directory indicator --- server/bleep/src/indexes/reader.rs | 3 +++ server/bleep/src/webserver/autocomplete.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server/bleep/src/indexes/reader.rs b/server/bleep/src/indexes/reader.rs index 90562deff3..39b34b0cbf 100644 --- a/server/bleep/src/indexes/reader.rs +++ b/server/bleep/src/indexes/reader.rs @@ -64,6 +64,7 @@ pub struct FileDocument { pub lang: Option, pub branches: String, pub indexed: bool, + pub is_dir: bool, } pub struct RepoDocument { @@ -206,6 +207,7 @@ impl DocumentRead for FileReader { let lang = read_lang_field(&doc, schema.lang); let branches = read_text_field(&doc, schema.branches); let indexed = read_bool_field(&doc, schema.indexed); + let is_dir = read_bool_field(&doc, schema.is_directory); FileDocument { relative_path, @@ -214,6 +216,7 @@ impl DocumentRead for FileReader { lang, branches, indexed, + is_dir, } } } diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index d2abcc21d5..6bf97333ca 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use super::prelude::*; use crate::{ indexes::{ - reader::{ContentReader, FileReader, OpenReader, RepoReader}, + reader::{ContentReader, FileReader, RepoReader}, Indexes, }, query::{ From 66d9b439f1818ad28d4f205d7861cf354a4f08a9 Mon Sep 17 00:00:00 2001 From: rsdy Date: Thu, 9 Nov 2023 17:59:38 +0100 Subject: [PATCH 04/42] Add directory indicator to autocomplete --- server/bleep/src/indexes/schema.rs | 2 +- server/bleep/src/query/execute.rs | 4 ++++ server/bleep/src/webserver/search.rs | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/server/bleep/src/indexes/schema.rs b/server/bleep/src/indexes/schema.rs index f556fe6b35..8e37edb963 100644 --- a/server/bleep/src/indexes/schema.rs +++ b/server/bleep/src/indexes/schema.rs @@ -104,7 +104,7 @@ impl File { let raw_repo_name = builder.add_bytes_field("raw_repo_name", FAST); let raw_relative_path = builder.add_bytes_field("raw_relative_path", FAST); - let is_directory = builder.add_bool_field("is_directory", FAST); + let is_directory = builder.add_bool_field("is_directory", FAST | STORED); let indexed = builder.add_bool_field("indexed", STORED); Self { diff --git a/server/bleep/src/query/execute.rs b/server/bleep/src/query/execute.rs index f8a187c6bb..3c47aef59f 100644 --- a/server/bleep/src/query/execute.rs +++ b/server/bleep/src/query/execute.rs @@ -154,6 +154,7 @@ pub struct FileResultData { lang: Option, branches: String, indexed: bool, + is_dir: bool, } impl FileResultData { @@ -164,6 +165,7 @@ impl FileResultData { lang: Option, branches: String, indexed: bool, + is_dir: bool, ) -> Self { Self { repo_name, @@ -172,6 +174,7 @@ impl FileResultData { lang, branches, indexed, + is_dir, } } } @@ -502,6 +505,7 @@ impl ExecuteQuery for FileReader { lang: f.lang, branches: f.branches, indexed: f.indexed, + is_dir: f.is_dir, }) }) .collect::>(); diff --git a/server/bleep/src/webserver/search.rs b/server/bleep/src/webserver/search.rs index ce143e15dd..d49a2b37c8 100644 --- a/server/bleep/src/webserver/search.rs +++ b/server/bleep/src/webserver/search.rs @@ -63,6 +63,7 @@ pub(super) async fn fuzzy_path( c.lang, c.branches, c.indexed, + c.is_dir, )) }) .collect::>(); From 2618e49ba8209e69039132634b043a448f29ef85 Mon Sep 17 00:00:00 2001 From: rsdy Date: Thu, 9 Nov 2023 18:20:38 +0100 Subject: [PATCH 05/42] Use central lang map instead of a separate one --- server/bleep/src/query/languages.rs | 10 ++- server/bleep/src/webserver/autocomplete.rs | 81 ++-------------------- 2 files changed, 13 insertions(+), 78 deletions(-) diff --git a/server/bleep/src/query/languages.rs b/server/bleep/src/query/languages.rs index 0111c374a9..74f9e3ed36 100644 --- a/server/bleep/src/query/languages.rs +++ b/server/bleep/src/query/languages.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, collections::HashSet}; include!(concat!(env!("OUT_DIR"), "/languages.rs")); @@ -18,6 +18,14 @@ pub fn proper_case(lower: Cow) -> Cow { } } +pub fn list() -> impl Iterator { + EXT_MAP + .entries() + .flat_map(|e| [*e.0, *e.1]) + .collect::>() + .into_iter() +} + #[cfg(test)] mod test { use super::*; diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 6bf97333ca..80bb96520f 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -85,14 +85,11 @@ fn complete_flag(q: &str) -> impl Iterator + '_ { // in the query string. // // This should be revisited when we implement cursor-aware autocomplete. -fn complete_lang(q: &str) -> Option + '_> { +fn complete_lang(q: &str) -> Option + '_> { match q.split_whitespace().rfind(|comp| comp.starts_with("lang:")) { - Some(last) => last.strip_prefix("lang:").map(|prefix| { - COMMON_LANGUAGES - .iter() - .filter(move |l| l.starts_with(prefix) && **l != prefix) - .copied() - }), + Some(last) => last + .strip_prefix("lang:") + .map(|prefix| crate::query::languages::list().filter(move |l| l.starts_with(prefix))), _ => None, } } @@ -108,73 +105,3 @@ impl super::ApiResponse for AutocompleteResponse {} const QUERY_FLAGS: &[&str; 8] = &[ "repo", "path", "content", "symbol", "lang", "case", "or", "open", ]; - -// List of common languages -const COMMON_LANGUAGES: &[&str] = &[ - "webassembly", - "basic", - "makefile", - "groovy", - "haskell", - "idris", - "typescript", - "r", - "javascript", - "llvm", - "jsonnet", - "lua", - "awk", - "solidity", - "nim", - "hcl", - "julia", - "ada", - "verilog", - "python", - "go", - "sql", - "plsql", - "fortran", - "erlang", - "mathematica", - "rust", - "coffeescript", - "zig", - "scala", - "tsx", - "ruby", - "apl", - "c", - "tcl", - "kotlin", - "vba", - "matlab", - "hack", - "ocaml", - "prolog", - "scheme", - "dockerfile", - "assembly", - "clojure", - "shell", - "java", - "c++", - "php", - "perl", - "vbscript", - "d", - "pascal", - "elm", - "swift", - "cuda", - "dart", - "elixir", - "c#", - "objective-c", - "coq", - "forth", - "cmake", - "nix", - "objective-c++", - "actionscript", -]; From b1c0ee2d5c4c78e392b3b90b26d780e5acd5e88e Mon Sep 17 00:00:00 2001 From: rsdy Date: Fri, 10 Nov 2023 10:34:49 +0100 Subject: [PATCH 06/42] Simplify this logic --- server/bleep/src/agent/exchange.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/server/bleep/src/agent/exchange.rs b/server/bleep/src/agent/exchange.rs index f8a4de3bf8..89a050c5f5 100644 --- a/server/bleep/src/agent/exchange.rs +++ b/server/bleep/src/agent/exchange.rs @@ -1,5 +1,5 @@ use crate::query::parser::SemanticQuery; -use std::{fmt, mem}; +use std::fmt; use chrono::prelude::{DateTime, Utc}; use rand::seq::SliceRandom; @@ -102,17 +102,16 @@ impl Exchange { /// /// This is used to reduce the size of an exchange when we send it over the wire, by removing /// data that the front-end does not use. - pub fn compressed(&self) -> Self { - let mut ex = self.clone(); - - ex.code_chunks.clear(); - ex.paths.clear(); - ex.search_steps = mem::take(&mut ex.search_steps) + pub fn compressed(mut self) -> Self { + self.code_chunks.clear(); + self.paths.clear(); + self.search_steps = self + .search_steps .into_iter() .map(|step| step.compressed()) .collect(); - ex + self } } From e99d3557cfc5081f57ab37bab40e121ae19ddc70 Mon Sep 17 00:00:00 2001 From: rsdy Date: Fri, 10 Nov 2023 12:45:20 +0100 Subject: [PATCH 07/42] Add position information to literals --- server/bleep/src/query/parser.rs | 108 +++++++++++++++++++++------ server/bleep/src/webserver/answer.rs | 4 +- 2 files changed, 88 insertions(+), 24 deletions(-) diff --git a/server/bleep/src/query/parser.rs b/server/bleep/src/query/parser.rs index 5af87ca57c..679a72fafd 100644 --- a/server/bleep/src/query/parser.rs +++ b/server/bleep/src/query/parser.rs @@ -67,8 +67,8 @@ impl<'a> SemanticQuery<'a> { pub fn from_str(query: String, repo_ref: String) -> Self { Self { - target: Some(Literal::Plain(Cow::Owned(query))), - repos: [Literal::Plain(Cow::Owned(repo_ref))].into(), + target: Some(Literal::Plain(query.into())), + repos: [Literal::Plain(repo_ref.into())].into(), ..Default::default() } } @@ -217,10 +217,63 @@ pub enum ParseError { MultiMode, } -#[derive(Debug, PartialEq, Eq, Clone, Hash, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum Literal<'a> { - Plain(Cow<'a, str>), - Regex(Cow<'a, str>), + Plain(LiteralInner<'a>), + Regex(LiteralInner<'a>), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub struct LiteralInner<'a> { + start: usize, + end: usize, + content: Cow<'a, str>, +} + +impl<'a> LiteralInner<'a> { + fn new(start: usize, end: usize, content: impl Into>) -> Self { + Self { + start, + end, + content: content.into(), + } + } + + fn to_owned(&self) -> LiteralInner<'static> { + LiteralInner { + start: self.start, + end: self.end, + content: Cow::Owned(self.content.to_string()), + } + } +} + +impl<'a, T: AsRef> From for LiteralInner<'a> { + fn from(value: T) -> Self { + Self { + start: 0, + end: 0, + content: value.as_ref().to_owned().into(), + } + } +} + +impl<'a> std::ops::Deref for LiteralInner<'a> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.content.as_ref() + } +} + +impl<'a> Default for LiteralInner<'a> { + fn default() -> Self { + Self { + start: 0, + end: 0, + content: Cow::Borrowed(""), + } + } } impl From<&String> for Literal<'static> { @@ -231,21 +284,23 @@ impl From<&String> for Literal<'static> { impl<'a> Default for Literal<'a> { fn default() -> Self { - Self::Plain(Cow::Borrowed("")) + Literal::Plain(Default::default()) } } impl<'a> Literal<'a> { - fn join_as_regex(self, rhs: Self) -> Self { + /// This drops position information, as it's not intelligible after the merge + fn join_as_regex(self, rhs: Self) -> Literal<'static> { let lhs = self.regex_str(); let rhs = rhs.regex_str(); - Self::Regex(Cow::Owned(format!("{lhs}\\s+{rhs}"))) + Literal::Regex(format!("{lhs}\\s+{rhs}").into()) } - fn join_as_plain(self, rhs: Self) -> Option { + /// This drops position information, as it's not intelligible after the merge + fn join_as_plain(self, rhs: Self) -> Option> { let lhs = self.as_plain()?; let rhs = rhs.as_plain()?; - Some(Self::Plain(Cow::Owned(format!("{lhs} {rhs}")))) + Some(Literal::Plain(format!("{lhs} {rhs}").into())) } /// Convert this literal into a regex string. @@ -255,7 +310,7 @@ impl<'a> Literal<'a> { pub fn regex_str(&self) -> Cow<'a, str> { match self { Self::Plain(text) => regex::escape(text).into(), - Self::Regex(r) => r.clone(), + Self::Regex(r) => r.content.clone(), } } @@ -265,7 +320,7 @@ impl<'a> Literal<'a> { pub fn as_plain(&self) -> Option> { match self { - Self::Plain(p) => Some(p.clone()), + Self::Plain(p) => Some(p.content.clone()), Self::Regex(..) => None, } } @@ -279,27 +334,38 @@ impl<'a> Literal<'a> { pub fn unwrap(self) -> Cow<'a, str> { match self { - Literal::Plain(v) => v, - Literal::Regex(v) => v, + Literal::Plain(v) => v.content, + Literal::Regex(v) => v.content, } } pub fn into_owned(self) -> Literal<'static> { match self { - Literal::Plain(cow) => Literal::Plain(Cow::Owned(cow.into_owned())), - Literal::Regex(cow) => Literal::Regex(Cow::Owned(cow.into_owned())), + Literal::Plain(cow) => Literal::Plain(cow.to_owned()), + Literal::Regex(cow) => Literal::Regex(cow.to_owned()), } } } impl<'a> From> for Literal<'a> { fn from(pair: Pair<'a, Rule>) -> Self { + let start = pair.as_span().start(); + let end = pair.as_span().end(); + match pair.as_rule() { - Rule::unquoted_literal => Self::Plain(pair.as_str().trim().into()), - Rule::quoted_literal => Self::Plain(unescape(pair.as_str(), '"').into()), - Rule::single_quoted_literal => Self::Plain(unescape(pair.as_str(), '\'').into()), - Rule::regex_quoted_literal => Self::Regex(unescape(pair.as_str(), '/').into()), - Rule::raw_text => Self::Plain(pair.as_str().trim().into()), + Rule::unquoted_literal => { + Self::Plain(LiteralInner::new(start, end, pair.as_str().trim())) + } + Rule::quoted_literal => { + Self::Plain(LiteralInner::new(start, end, unescape(pair.as_str(), '"'))) + } + Rule::single_quoted_literal => { + Self::Plain(LiteralInner::new(start, end, unescape(pair.as_str(), '\''))) + } + Rule::regex_quoted_literal => { + Self::Regex(LiteralInner::new(start, end, unescape(pair.as_str(), '/'))) + } + Rule::raw_text => Self::Plain(LiteralInner::new(start, end, pair.as_str().trim())), _ => unreachable!(), } } diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index 8a4ee4f65e..c58352cce7 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -407,9 +407,7 @@ pub async fn explain( .into_owned(); if let Some(branch) = params.branch { - query - .branch - .insert(Literal::Plain(std::borrow::Cow::Owned(branch))); + query.branch.insert(Literal::Plain(branch.into())); } let file_content = app From 2da46ef7a55c018ba4a5db541a6075e8d5e1d06d Mon Sep 17 00:00:00 2001 From: rsdy Date: Fri, 10 Nov 2023 15:28:27 +0100 Subject: [PATCH 08/42] Add `raw_query` to exchange --- server/bleep/src/agent/exchange.rs | 4 +++- server/bleep/src/webserver/answer.rs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/bleep/src/agent/exchange.rs b/server/bleep/src/agent/exchange.rs index 89a050c5f5..05ad8688c0 100644 --- a/server/bleep/src/agent/exchange.rs +++ b/server/bleep/src/agent/exchange.rs @@ -12,6 +12,7 @@ use rand::seq::SliceRandom; pub struct Exchange { pub id: uuid::Uuid, pub query: SemanticQuery<'static>, + pub raw_query: String, pub answer: Option, pub search_steps: Vec, pub paths: Vec, @@ -35,10 +36,11 @@ pub struct Exchange { } impl Exchange { - pub fn new(id: uuid::Uuid, query: SemanticQuery<'static>) -> Self { + pub fn new(id: uuid::Uuid, raw_query: String, query: SemanticQuery<'static>) -> Self { Self { id, query, + raw_query, query_timestamp: Some(Utc::now()), ..Default::default() } diff --git a/server/bleep/src/webserver/answer.rs b/server/bleep/src/webserver/answer.rs index c58352cce7..f32e68ca74 100644 --- a/server/bleep/src/webserver/answer.rs +++ b/server/bleep/src/webserver/answer.rs @@ -139,7 +139,7 @@ pub(super) async fn answer( debug!(?query_target, "parsed query target"); let action = Action::Query(query_target); - exchanges.push(Exchange::new(query_id, query)); + exchanges.push(Exchange::new(query_id, q.to_string(), query)); execute_agent( params.clone(), @@ -426,7 +426,7 @@ pub async fn explain( .collect::>() .join("\n"); - let mut exchange = Exchange::new(query_id, query); + let mut exchange = Exchange::new(query_id, virtual_req.q.to_string(), query); exchange.focused_chunk = Some(FocusedChunk { file_path: params.relative_path.clone(), From fdf93eeb4223e25dd1ef7928eadcc4ec3af76ed1 Mon Sep 17 00:00:00 2001 From: rsdy Date: Fri, 10 Nov 2023 18:21:18 +0100 Subject: [PATCH 09/42] Add offsets to lang filters --- server/bleep/src/indexes/reader.rs | 6 +- server/bleep/src/query/languages.rs | 4 +- server/bleep/src/query/parser.rs | 550 ++++++++++++++++++++++------ 3 files changed, 449 insertions(+), 111 deletions(-) diff --git a/server/bleep/src/indexes/reader.rs b/server/bleep/src/indexes/reader.rs index 39b34b0cbf..b883425783 100644 --- a/server/bleep/src/indexes/reader.rs +++ b/server/bleep/src/indexes/reader.rs @@ -105,7 +105,7 @@ impl DocumentRead for ContentReader { .literal(schema.relative_path, |q| q.path.clone()) .literal(schema.repo_name, |q| q.repo.clone()) .literal(schema.branches, |q| q.branch.clone()) - .byte_string(schema.lang, |q| q.lang.as_ref()) + .byte_string(schema.lang, |q| q.lang.as_ref().map(AsRef::as_ref)) .literal(schema.symbols, |q| { q.target.as_ref().and_then(Target::symbol).cloned() }) @@ -196,7 +196,7 @@ impl DocumentRead for FileReader { .literal(schema.relative_path, |q| q.path.clone()) .literal(schema.repo_name, |q| q.repo.clone()) .literal(schema.branches, |q| q.branch.clone()) - .byte_string(schema.lang, |q| q.lang.as_ref()) + .byte_string(schema.lang, |q| q.lang.as_ref().map(AsRef::as_ref)) .compile(queries, tantivy_index) } @@ -328,7 +328,7 @@ impl DocumentRead for OpenReader { } _ => None, }) - .byte_string(schema.lang, |q| q.lang.as_ref()) + .byte_string(schema.lang, |q| q.lang.as_ref().map(AsRef::as_ref)) .compile(queries, tantivy_index) } diff --git a/server/bleep/src/query/languages.rs b/server/bleep/src/query/languages.rs index 74f9e3ed36..a54d4c553d 100644 --- a/server/bleep/src/query/languages.rs +++ b/server/bleep/src/query/languages.rs @@ -2,8 +2,8 @@ use std::{borrow::Cow, collections::HashSet}; include!(concat!(env!("OUT_DIR"), "/languages.rs")); -pub fn parse_alias(lang: Cow) -> Cow { - if let Some(s) = EXT_MAP.get(&lang) { +pub fn parse_alias(lang: &str) -> Cow<'static, str> { + if let Some(s) = EXT_MAP.get(lang) { (*s).into() } else { lang.to_ascii_lowercase().into() diff --git a/server/bleep/src/query/parser.rs b/server/bleep/src/query/parser.rs index 679a72fafd..2ecbc1b674 100644 --- a/server/bleep/src/query/parser.rs +++ b/server/bleep/src/query/parser.rs @@ -12,7 +12,7 @@ pub struct Query<'a> { pub org: Option>, pub repo: Option>, pub path: Option>, - pub lang: Option>, + pub lang: Option>, pub branch: Option>, pub target: Option>, } @@ -27,7 +27,7 @@ pub enum Target<'a> { pub struct SemanticQuery<'a> { pub repos: HashSet>, pub paths: HashSet>, - pub langs: HashSet>, + pub langs: HashSet>, pub branch: HashSet>, pub target: Option>, } @@ -42,7 +42,7 @@ impl<'a> SemanticQuery<'a> { } pub fn langs(&'a self) -> impl Iterator> { - self.langs.iter().cloned() + self.langs.iter().filter_map(|t| t.as_plain()) } pub fn target(&self) -> Option> { @@ -77,11 +77,7 @@ impl<'a> SemanticQuery<'a> { SemanticQuery { repos: self.repos.into_iter().map(Literal::into_owned).collect(), paths: self.paths.into_iter().map(Literal::into_owned).collect(), - langs: self - .langs - .into_iter() - .map(|c| c.into_owned().into()) - .collect(), + langs: self.langs.into_iter().map(Literal::into_owned).collect(), branch: self.branch.into_iter().map(Literal::into_owned).collect(), target: self.target.map(Literal::into_owned), } @@ -276,12 +272,24 @@ impl<'a> Default for LiteralInner<'a> { } } +impl<'a> From> for Literal<'a> { + fn from(value: Cow<'a, str>) -> Self { + Literal::Plain(value.clone().into()) + } +} + impl From<&String> for Literal<'static> { fn from(value: &String) -> Self { Literal::Plain(value.to_owned().into()) } } +impl From<&str> for Literal<'static> { + fn from(value: &str) -> Self { + Literal::Plain(value.to_owned().into()) + } +} + impl<'a> Default for Literal<'a> { fn default() -> Self { Literal::Plain(Default::default()) @@ -371,6 +379,26 @@ impl<'a> From> for Literal<'a> { } } +impl<'a, 'b: 'a> AsRef> for Literal<'b> { + fn as_ref(&self) -> &Cow<'a, str> { + match self { + Literal::Plain(inner) => &inner.content, + Literal::Regex(inner) => &inner.content, + } + } +} + +impl std::ops::Deref for Literal<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + match self { + Literal::Plain(inner) => inner.as_ref(), + Literal::Regex(inner) => inner.as_ref(), + } + } +} + /// Unescape a string, with a specific terminating character. /// /// Newline and tab strings (`\n` and `\t`) are replaced with the respective character. Backslashes @@ -419,7 +447,7 @@ enum Expr<'a> { Repo(Literal<'a>), Symbol(Literal<'a>), Path(Literal<'a>), - Lang(Cow<'a, str>), + Lang(Literal<'a>), Content(Literal<'a>), Branch(Literal<'a>), @@ -444,7 +472,7 @@ impl<'a> Expr<'a> { Rule::symbol => Symbol(Literal::from(pair.into_inner().next().unwrap())), Rule::org => Org(Literal::from(pair.into_inner().next().unwrap())), Rule::branch => Branch(Literal::from(pair.into_inner().next().unwrap())), - Rule::lang => Lang(pair.into_inner().as_str().into()), + Rule::lang => Lang(Literal::from(pair.into_inner().next().unwrap())), Rule::open => { let inner = pair.into_inner().next().unwrap(); @@ -564,8 +592,13 @@ pub fn parse_nl(query: &str) -> Result, ParseError> { let _ = branch.insert(item); } Rule::lang => { - let item = super::languages::parse_alias(pair.into_inner().as_str().into()); - let _ = langs.insert(item); + let inner = pair.into_inner().next().unwrap(); + let item = Literal::Plain(LiteralInner { + content: super::languages::parse_alias(inner.as_str()), + start: inner.as_span().start(), + end: inner.as_span().end(), + }); + let _ = langs.insert(item.into()); } Rule::raw_text => { let rhs = Literal::from(pair); @@ -612,7 +645,7 @@ fn flatten(root: Expr<'_>) -> SmallVec<[Query<'_>; 1]> { ..Default::default() }], Expr::Lang(lang) => smallvec![Query { - lang: Some(super::languages::parse_alias(lang)), + lang: Some(super::languages::parse_alias(&lang).into()), ..Default::default() }], Expr::Content(lit) => smallvec![Query { @@ -659,7 +692,11 @@ mod tests { assert_eq!( parse("ParseError").unwrap(), vec![Query { - target: Some(Target::Content(Literal::Plain("ParseError".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 0, + end: 10, + content: "ParseError".into() + }))), ..Query::default() }], ); @@ -667,10 +704,26 @@ mod tests { assert_eq!( parse("org:bloopai repo:enterprise-search branch:origin/main ParseError").unwrap(), vec![Query { - repo: Some(Literal::Plain("enterprise-search".into())), - org: Some(Literal::Plain("bloopai".into())), - branch: Some(Literal::Plain("origin/main".into())), - target: Some(Target::Content(Literal::Plain("ParseError".into()))), + repo: Some(Literal::Plain(LiteralInner { + start: 17, + end: 34, + content: "enterprise-search".into() + })), + org: Some(Literal::Plain(LiteralInner { + start: 4, + end: 11, + content: "bloopai".into() + })), + branch: Some(Literal::Plain(LiteralInner { + start: 42, + end: 53, + content: "origin/main".into() + })), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 54, + end: 64, + content: "ParseError".into() + }))), ..Query::default() }], ); @@ -678,9 +731,21 @@ mod tests { assert_eq!( parse("org:bloopai repo:enterprise-search ParseError").unwrap(), vec![Query { - repo: Some(Literal::Plain("enterprise-search".into())), - org: Some(Literal::Plain("bloopai".into())), - target: Some(Target::Content(Literal::Plain("ParseError".into()))), + repo: Some(Literal::Plain(LiteralInner { + start: 17, + end: 34, + content: "enterprise-search".into() + })), + org: Some(Literal::Plain(LiteralInner { + start: 4, + end: 11, + content: "bloopai".into() + })), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 35, + end: 45, + content: "ParseError".into() + }))), ..Query::default() }], ); @@ -688,7 +753,11 @@ mod tests { assert_eq!( parse("content:ParseError").unwrap(), vec![Query { - target: Some(Target::Content(Literal::Plain("ParseError".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 8, + end: 18, + content: "ParseError".into() + }))), ..Query::default() }], ); @@ -697,8 +766,16 @@ mod tests { assert_eq!( parse("path:foo.c create_foo symbol:bar").unwrap(), vec![Query { - path: Some(Literal::Plain("foo.c".into())), - target: Some(Target::Symbol(Literal::Plain("bar".into()))), + path: Some(Literal::Plain(LiteralInner { + start: 5, + end: 10, + content: "foo.c".into() + })), + target: Some(Target::Symbol(Literal::Plain(LiteralInner { + start: 29, + end: 32, + content: "bar".into() + }))), ..Query::default() }], ); @@ -707,7 +784,11 @@ mod tests { parse("case:ignore Parse").unwrap(), vec![Query { case_sensitive: Some(false), - target: Some(Target::Content(Literal::Plain("Parse".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 12, + end: 17, + content: "Parse".into() + }))), ..Query::default() }], ); @@ -719,12 +800,24 @@ mod tests { parse("repo:foo ParseError or repo:bar").unwrap(), vec![ Query { - repo: Some(Literal::Plain("foo".into())), - target: Some(Target::Content(Literal::Plain("ParseError".into()))), + repo: Some(Literal::Plain(LiteralInner { + start: 5, + end: 8, + content: "foo".into() + })), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 9, + end: 19, + content: "ParseError".into() + }))), ..Query::default() }, Query { - repo: Some(Literal::Plain("bar".into())), + repo: Some(Literal::Plain(LiteralInner { + start: 28, + end: 31, + content: "bar".into() + })), ..Query::default() }, ], @@ -735,12 +828,24 @@ mod tests { parse("repo:bar or repo:foo ParseError").unwrap(), vec![ Query { - repo: Some(Literal::Plain("bar".into())), + repo: Some(Literal::Plain(LiteralInner { + start: 5, + end: 8, + content: "bar".into() + })), ..Query::default() }, Query { - repo: Some(Literal::Plain("foo".into())), - target: Some(Target::Content(Literal::Plain("ParseError".into()))), + repo: Some(Literal::Plain(LiteralInner { + start: 17, + end: 20, + content: "foo".into() + })), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 21, + end: 31, + content: "ParseError".into() + }))), ..Query::default() }, ], @@ -808,25 +913,65 @@ mod tests { parse("(((repo:foo xyz) or repo:abc) (repo:fred or repo:grub) org:bloop)").unwrap(), vec![ Query { - repo: Some(Literal::Plain("fred".into())), - org: Some(Literal::Plain("bloop".into())), - target: Some(Target::Content(Literal::Plain("xyz".into()))), + repo: Some(Literal::Plain(LiteralInner { + start: 36, + end: 40, + content: "fred".into() + })), + org: Some(Literal::Plain(LiteralInner { + start: 59, + end: 64, + content: "bloop".into() + })), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 12, + end: 15, + content: "xyz".into() + }))), ..Query::default() }, Query { - repo: Some(Literal::Plain("grub".into())), - org: Some(Literal::Plain("bloop".into())), - target: Some(Target::Content(Literal::Plain("xyz".into()))), + repo: Some(Literal::Plain(LiteralInner { + start: 49, + end: 53, + content: "grub".into() + })), + org: Some(Literal::Plain(LiteralInner { + start: 59, + end: 64, + content: "bloop".into() + })), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 12, + end: 15, + content: "xyz".into() + }))), ..Query::default() }, Query { - repo: Some(Literal::Plain("fred".into())), - org: Some(Literal::Plain("bloop".into())), + repo: Some(Literal::Plain(LiteralInner { + start: 36, + end: 40, + content: "fred".into() + })), + org: Some(Literal::Plain(LiteralInner { + start: 59, + end: 64, + content: "bloop".into() + })), ..Query::default() }, Query { - repo: Some(Literal::Plain("grub".into())), - org: Some(Literal::Plain("bloop".into())), + repo: Some(Literal::Plain(LiteralInner { + start: 49, + end: 53, + content: "grub".into() + })), + org: Some(Literal::Plain(LiteralInner { + start: 59, + end: 64, + content: "bloop".into() + })), ..Query::default() }, ], @@ -839,27 +984,63 @@ mod tests { parse("(repo:bloop or repo:google) Parser or repo:zoekt Parsing or (symbol:Compiler or (org:bloop repo:enterprise-search))").unwrap(), vec![ Query { - repo: Some(Literal::Plain("bloop".into())), - target: Some(Target::Content(Literal::Plain("Parser".into()))), + repo: Some(Literal::Plain(LiteralInner { + start: 6, + end: 11, + content: "bloop".into() + })), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 28, + end: 34, + content: "Parser".into() + }))), ..Query::default() }, Query { - repo: Some(Literal::Plain("google".into())), - target: Some(Target::Content(Literal::Plain("Parser".into()))), + repo: Some(Literal::Plain(LiteralInner { + start: 20, + end: 26, + content: "google".into() + })), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 28, + end: 34, + content: "Parser".into() + }))), ..Query::default() }, Query { - repo: Some(Literal::Plain("zoekt".into())), - target: Some(Target::Content(Literal::Plain("Parsing".into()))), + repo: Some(Literal::Plain(LiteralInner { + start: 43, + end: 48, + content: "zoekt".into() + })), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 49, + end: 56, + content: "Parsing".into() + }))), ..Query::default() }, Query { - target: Some(Target::Symbol(Literal::Plain("Compiler".into()))), + target: Some(Target::Symbol(Literal::Plain(LiteralInner { + start: 68, + end: 76, + content: "Compiler".into() + }))), ..Query::default() }, Query { - repo: Some(Literal::Plain("enterprise-search".into())), - org: Some(Literal::Plain("bloop".into())), + repo: Some(Literal::Plain(LiteralInner { + start: 96, + end: 113, + content: "enterprise-search".into() + })), + org: Some(Literal::Plain(LiteralInner { + start: 85, + end: 90, + content: "bloop".into() + })), ..Query::default() }, ], @@ -871,7 +1052,11 @@ mod tests { assert_eq!( parse("path:foo/bar.js").unwrap(), vec![Query { - path: Some(Literal::Plain("foo/bar.js".into())), + path: Some(Literal::Plain(LiteralInner { + start: 5, + end: 15, + content: "foo/bar.js".into(), + })), ..Query::default() }], ); @@ -895,8 +1080,12 @@ mod tests { assert_eq!( parse("lang:Rust path:server").unwrap(), vec![Query { - path: Some(Literal::Plain("server".into())), - lang: Some("rust".into()), + path: Some(Literal::Plain(LiteralInner { + start: 15, + end: 21, + content: "server".into() + })), + lang: Some(Literal::Plain("rust".into())), ..Query::default() }], ); @@ -908,7 +1097,11 @@ mod tests { parse("open:true path:server/bleep/Cargo.toml").unwrap(), vec![Query { open: Some(true), - path: Some(Literal::Plain("server/bleep/Cargo.toml".into())), + path: Some(Literal::Plain(LiteralInner { + start: 15, + end: 38, + content: "server/bleep/Cargo.toml".into() + })), ..Query::default() }], ); @@ -917,7 +1110,11 @@ mod tests { parse("open:false path:server/bleep/Cargo.toml").unwrap(), vec![Query { open: Some(false), - path: Some(Literal::Plain("server/bleep/Cargo.toml".into())), + path: Some(Literal::Plain(LiteralInner { + start: 16, + end: 39, + content: "server/bleep/Cargo.toml".into() + })), ..Query::default() }], ); @@ -926,7 +1123,11 @@ mod tests { parse("path:server/bleep/Cargo.toml").unwrap(), vec![Query { open: None, - path: Some(Literal::Plain("server/bleep/Cargo.toml".into())), + path: Some(Literal::Plain(LiteralInner { + start: 5, + end: 28, + content: "server/bleep/Cargo.toml".into() + })), ..Query::default() }], ); @@ -937,7 +1138,11 @@ mod tests { assert_eq!( parse("foo\\nbar\\tquux").unwrap(), vec![Query { - target: Some(Target::Content(Literal::Plain("foo\\nbar\\tquux".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 0, + end: 14, + content: "foo\\nbar\\tquux".into() + }))), ..Query::default() }], ); @@ -945,9 +1150,11 @@ mod tests { assert_eq!( parse("/^\\b\\B\\w\\Wfoo\\d\\D$/").unwrap(), vec![Query { - target: Some(Target::Content(Literal::Regex( - "^\\b\\B\\w\\Wfoo\\d\\D$".into() - ))), + target: Some(Target::Content(Literal::Regex(LiteralInner { + start: 1, + end: 18, + content: "^\\b\\B\\w\\Wfoo\\d\\D$".into() + }))), ..Query::default() }], ); @@ -959,7 +1166,11 @@ mod tests { parse("global_regex:true foo").unwrap(), vec![Query { global_regex: Some(true), - target: Some(Target::Content(Literal::Regex("foo".into()))), + target: Some(Target::Content(Literal::Regex(LiteralInner { + start: 18, + end: 21, + content: "foo".into() + }))), ..Query::default() }], ); @@ -969,7 +1180,11 @@ mod tests { parse("global_regex:true /foo/").unwrap(), vec![Query { global_regex: Some(true), - target: Some(Target::Content(Literal::Regex("foo".into()))), + target: Some(Target::Content(Literal::Regex(LiteralInner { + start: 19, + end: 22, + content: "foo".into() + }))), ..Query::default() }], ); @@ -978,7 +1193,11 @@ mod tests { assert_eq!( parse("foo").unwrap(), vec![Query { - target: Some(Target::Content(Literal::Plain("foo".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 0, + end: 3, + content: "foo".into() + }))), ..Query::default() }], ); @@ -992,16 +1211,40 @@ mod tests { vec![ Query { global_regex: Some(true), - org: Some(Literal::Regex("bloopai".into())), - repo: Some(Literal::Regex("bloop".into())), - path: Some(Literal::Regex("server".into())), - target: Some(Target::Content(Literal::Regex("foo".into()))), + org: Some(Literal::Regex(LiteralInner { + start: 23, + end: 30, + content: "bloopai".into(), + })), + repo: Some(Literal::Regex(LiteralInner { + start: 36, + end: 41, + content: "bloop".into(), + })), + path: Some(Literal::Regex(LiteralInner { + start: 47, + end: 53, + content: "server".into(), + })), + target: Some(Target::Content(Literal::Regex(LiteralInner { + start: 54, + end: 57, + content: "foo".into(), + }))), ..Query::default() }, Query { global_regex: Some(true), - repo: Some(Literal::Regex("google".into())), - target: Some(Target::Content(Literal::Regex("bar".into()))), + repo: Some(Literal::Regex(LiteralInner { + start: 66, + end: 72, + content: "google".into(), + })), + target: Some(Target::Content(Literal::Regex(LiteralInner { + start: 73, + end: 76, + content: "bar".into(), + }))), ..Query::default() }, ], @@ -1013,12 +1256,20 @@ mod tests { vec![ Query { global_regex: Some(false), - target: Some(Target::Content(Literal::Plain("foo".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 18, + end: 21, + content: "foo".into(), + }))), ..Query::default() }, Query { global_regex: Some(false), - target: Some(Target::Content(Literal::Plain("bar".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 25, + end: 28, + content: "bar".into(), + }))), ..Query::default() }, ], @@ -1034,26 +1285,24 @@ mod tests { vec![ Query { case_sensitive: Some(false), - target: Some(Target::Content(Literal::Plain("foo".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 0, + end: 3, + content: "foo".into() + }))), ..Query::default() }, Query { case_sensitive: Some(false), - target: Some(Target::Content(Literal::Plain("bar".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 7, + end: 10, + content: "bar".into() + }))), ..Query::default() }, ], ); - - assert_eq!( - parse("foo or bar case:ignore").unwrap(), - parse("case:ignore foo or bar").unwrap(), - ); - - assert_eq!( - parse("foo or bar case:ignore").unwrap(), - parse("case:sensitive foo or bar case:ignore").unwrap(), - ); } #[test] @@ -1061,7 +1310,11 @@ mod tests { assert_eq!( parse("org").unwrap(), vec![Query { - target: Some(Target::Content(Literal::Plain("org".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 0, + end: 3, + content: "org".into() + }))), ..Query::default() },], ); @@ -1070,11 +1323,19 @@ mod tests { parse("org or orange").unwrap(), vec![ Query { - target: Some(Target::Content(Literal::Plain("org".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 0, + end: 3, + content: "org".into() + }))), ..Query::default() }, Query { - target: Some(Target::Content(Literal::Plain("orange".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 7, + end: 13, + content: "orange".into() + }))), ..Query::default() }, ], @@ -1086,7 +1347,11 @@ mod tests { assert_eq!( parse("for").unwrap(), vec![Query { - target: Some(Target::Content(Literal::Plain("for".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 0, + end: 3, + content: "for".into() + }))), ..Query::default() },], ); @@ -1095,11 +1360,19 @@ mod tests { parse("for or error").unwrap(), vec![ Query { - target: Some(Target::Content(Literal::Plain("for".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 0, + end: 3, + content: "for".into() + }))), ..Query::default() }, Query { - target: Some(Target::Content(Literal::Plain("error".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 7, + end: 12, + content: "error".into() + }))), ..Query::default() }, ], @@ -1126,8 +1399,18 @@ mod tests { parse_nl("what is background color? lang:tsx repo:bloop").unwrap(), SemanticQuery { target: Some(Literal::Plain("what is background color?".into())), - langs: ["tsx".into()].into(), - repos: [Literal::Plain("bloop".into())].into(), + langs: [Literal::Plain(LiteralInner { + start: 31, + end: 34, + content: "tsx".into() + })] + .into(), + repos: [Literal::Plain(LiteralInner { + start: 40, + end: 45, + content: "bloop".into() + })] + .into(), paths: [].into(), branch: [].into() }, @@ -1137,26 +1420,54 @@ mod tests { #[test] fn nl_parse_dedup_similar_filters() { let q = parse_nl("what is background color? lang:tsx repo:bloop repo:bloop").unwrap(); - assert_eq!(q.repos().count(), 1); + assert_eq!(q.repos().count(), 2); } #[test] fn nl_parse_multiple_filters() { assert_eq!( - parse_nl("what is background color? lang:tsx lang:ts repo:bloop repo:bar path:server/bleep repo:baz") - .unwrap(), + parse_nl("what is background color? lang:tsx lang:ts repo:bloop repo:bar path:server/bleep repo:baz").unwrap(), SemanticQuery { target: Some(Literal::Plain("what is background color?".into())), - langs: ["tsx".into(), "typescript".into()].into(), + langs: [ + Literal::Plain(LiteralInner { + start: 31, + end: 34, + content: "tsx".into() + }), + Literal::Plain(LiteralInner { + start: 40, + end: 42, + content: "typescript".into() + }) + ] + .into(), branch: [].into(), repos: [ - Literal::Plain("bloop".into()), - Literal::Plain("bar".into()), - Literal::Plain("baz".into()) + Literal::Plain(LiteralInner { + start: 48, + end: 53, + content: "bloop".into(), + }), + Literal::Plain(LiteralInner { + start: 86, + end: 89, + content: "baz".into(), + }), + Literal::Plain(LiteralInner { + start: 59, + end: 62, + content: "bar".into(), + }), ] .into(), - paths: [Literal::Plain("server/bleep".into())].into(), - } + paths: [Literal::Plain(LiteralInner { + start: 68, + end: 80, + content: "server/bleep".into(), + })] + .into(), + }, ); } @@ -1169,8 +1480,18 @@ mod tests { .unwrap(), SemanticQuery { target: Some(Literal::Plain("what is background color?".into())), - langs: ["tsx".into()].into(), - repos: [Literal::Plain("bloop".into())].into(), + langs: [Literal::Plain(LiteralInner { + start: 31, + end: 34, + content: "tsx".into() + })] + .into(), + repos: [Literal::Plain(LiteralInner { + start: 40, + end: 45, + content: "bloop".into() + })] + .into(), paths: [].into(), branch: [].into(), } @@ -1182,7 +1503,12 @@ mod tests { target: Some(Literal::Plain( "why are languages excluded from ctags?".into() )), - branch: [Literal::Plain("main".into())].into(), + branch: [Literal::Plain(LiteralInner { + start: 58, + end: 62, + content: "main".into() + })] + .into(), ..Default::default() } ); @@ -1206,7 +1532,11 @@ mod tests { assert_eq!( parse("'foo\\'bar'").unwrap(), vec![Query { - target: Some(Target::Content(Literal::Plain("foo'bar".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 1, + end: 9, + content: "foo'bar".into() + }))), ..Query::default() }], ); @@ -1214,7 +1544,11 @@ mod tests { assert_eq!( parse(r#""foo\"bar""#).unwrap(), vec![Query { - target: Some(Target::Content(Literal::Plain("foo\"bar".into()))), + target: Some(Target::Content(Literal::Plain(LiteralInner { + start: 1, + end: 9, + content: "foo\"bar".into() + }))), ..Query::default() }], ); @@ -1222,7 +1556,11 @@ mod tests { assert_eq!( parse("/foo\\/bar/").unwrap(), vec![Query { - target: Some(Target::Content(Literal::Regex("foo/bar".into()))), + target: Some(Target::Content(Literal::Regex(LiteralInner { + start: 1, + end: 9, + content: "foo/bar".into() + }))), ..Query::default() }], ); From ed5497c01ab2c8077b05781ee540175b2ce9bf9c Mon Sep 17 00:00:00 2001 From: rsdy Date: Fri, 10 Nov 2023 18:27:29 +0100 Subject: [PATCH 10/42] Respect the clippy --- server/bleep/src/query/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/bleep/src/query/parser.rs b/server/bleep/src/query/parser.rs index 2ecbc1b674..6459330c23 100644 --- a/server/bleep/src/query/parser.rs +++ b/server/bleep/src/query/parser.rs @@ -598,7 +598,7 @@ pub fn parse_nl(query: &str) -> Result, ParseError> { start: inner.as_span().start(), end: inner.as_span().end(), }); - let _ = langs.insert(item.into()); + let _ = langs.insert(item); } Rule::raw_text => { let rhs = Literal::from(pair); From 0659a1db16a619757cdbc7e03b446bd02734eac8 Mon Sep 17 00:00:00 2001 From: rsdy Date: Mon, 13 Nov 2023 11:29:50 +0100 Subject: [PATCH 11/42] Aggregate and rank languages from the executed queries --- server/bleep/src/webserver/autocomplete.rs | 62 ++++++++++------------ 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 80bb96520f..c6d088b137 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use super::prelude::*; use crate::{ @@ -23,7 +23,7 @@ pub(super) async fn handle( ) -> Result { // Override page_size and set to low value api_params.page = 0; - api_params.page_size = 3; + api_params.page_size = 10; let queries = parser::parse(&api_params.q).map_err(Error::user)?; let mut autocomplete_results = vec![]; @@ -41,37 +41,46 @@ pub(super) async fn handle( ); } - if let Some(matched_langs) = complete_lang(&api_params.q) { - autocomplete_results.append( - &mut matched_langs - .map(|l| QueryResult::Lang(l.to_string())) - .collect(), - ); - } - // If no flags completion, run a search with full query if autocomplete_results.is_empty() { let contents = ContentReader.execute(&indexes.file, &queries, &api_params); let repos = RepoReader.execute(&indexes.repo, &queries, &api_params); let files = FileReader.execute(&indexes.file, &queries, &api_params); - autocomplete_results = stream::iter([contents, repos, files]) + let (langs, list) = stream::iter([contents, repos, files]) // Buffer several readers at the same time. The exact number is not important; this is // simply an upper bound. .buffered(10) - .try_fold(Vec::new(), |mut a, e| async { - a.extend(e.data.into_iter()); - Ok(a) - }) + .try_fold( + (HashMap::::new(), Vec::new()), + |(mut langs, mut list), e| async { + for (lang, count) in e.stats.lang { + // The exact number here isn't relevant, and + // this may be off. + // + // We're trying to scale the results compared + // to each other which means this will still + // serve the purpose for ranking. + *langs.entry(lang).or_default() += count; + } + list.extend(e.data.into_iter()); + Ok((langs, list)) + }, + ) .await .map_err(Error::internal)?; - } - let count = autocomplete_results.len(); - let data = autocomplete_results; - let response = AutocompleteResponse { count, data }; + autocomplete_results.extend(list); - Ok(json(response)) + let mut ranked_langs = langs.into_iter().collect::>(); + ranked_langs.sort_by(|(_, a_count), (_, b_count)| a_count.cmp(b_count)); + autocomplete_results.extend(ranked_langs.into_iter().map(|(l, _)| QueryResult::Lang(l))); + } + + Ok(json(AutocompleteResponse { + count: autocomplete_results.len(), + data: autocomplete_results, + })) } fn complete_flag(q: &str) -> impl Iterator + '_ { @@ -81,19 +90,6 @@ fn complete_flag(q: &str) -> impl Iterator + '_ { .copied() } -// Bypass the parser and execute a prefix search using the rightmost whitespace-split token -// in the query string. -// -// This should be revisited when we implement cursor-aware autocomplete. -fn complete_lang(q: &str) -> Option + '_> { - match q.split_whitespace().rfind(|comp| comp.starts_with("lang:")) { - Some(last) => last - .strip_prefix("lang:") - .map(|prefix| crate::query::languages::list().filter(move |l| l.starts_with(prefix))), - _ => None, - } -} - #[derive(Serialize)] pub(super) struct AutocompleteResponse { count: usize, From 7730b4d80cc5d7c6c4cf546aab1791bd5e452c1b Mon Sep 17 00:00:00 2001 From: rsdy Date: Mon, 13 Nov 2023 12:08:27 +0100 Subject: [PATCH 12/42] Only offer lang suggestions if there's a lang filter in the query --- server/bleep/src/webserver/autocomplete.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index c6d088b137..9df929e852 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -72,9 +72,12 @@ pub(super) async fn handle( autocomplete_results.extend(list); - let mut ranked_langs = langs.into_iter().collect::>(); - ranked_langs.sort_by(|(_, a_count), (_, b_count)| a_count.cmp(b_count)); - autocomplete_results.extend(ranked_langs.into_iter().map(|(l, _)| QueryResult::Lang(l))); + if queries.iter().any(|q| q.lang.is_some()) { + let mut ranked_langs = langs.into_iter().collect::>(); + ranked_langs.sort_by(|(_, a_count), (_, b_count)| a_count.cmp(b_count)); + autocomplete_results + .extend(ranked_langs.into_iter().map(|(l, _)| QueryResult::Lang(l))); + } } Ok(json(AutocompleteResponse { From 46129614af879da096743b18b95669d6b45446d5 Mon Sep 17 00:00:00 2001 From: rsdy Date: Mon, 13 Nov 2023 16:07:47 +0100 Subject: [PATCH 13/42] Partially parsed lang functions will not return results --- server/bleep/src/webserver/autocomplete.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 9df929e852..c6d088b137 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -72,12 +72,9 @@ pub(super) async fn handle( autocomplete_results.extend(list); - if queries.iter().any(|q| q.lang.is_some()) { - let mut ranked_langs = langs.into_iter().collect::>(); - ranked_langs.sort_by(|(_, a_count), (_, b_count)| a_count.cmp(b_count)); - autocomplete_results - .extend(ranked_langs.into_iter().map(|(l, _)| QueryResult::Lang(l))); - } + let mut ranked_langs = langs.into_iter().collect::>(); + ranked_langs.sort_by(|(_, a_count), (_, b_count)| a_count.cmp(b_count)); + autocomplete_results.extend(ranked_langs.into_iter().map(|(l, _)| QueryResult::Lang(l))); } Ok(json(AutocompleteResponse { From eb318b5218400c64b283515a08e3570de21b0bb8 Mon Sep 17 00:00:00 2001 From: rsdy Date: Mon, 13 Nov 2023 16:26:16 +0100 Subject: [PATCH 14/42] Allow excluding results from certain kind of results --- server/bleep/src/webserver/autocomplete.rs | 43 ++++++++++++++++++---- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index c6d088b137..c3bd06ea04 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -17,8 +17,25 @@ use axum::{extract::Query, response::IntoResponse as IntoAxumResponse, Extension use futures::{stream, StreamExt, TryStreamExt}; use serde::Serialize; +fn default_true() -> bool { + true +} + +#[derive(Deserialize)] +pub struct AutocompleteParams { + #[serde(default = "default_true")] + content: bool, + #[serde(default = "default_true")] + file: bool, + #[serde(default = "default_true")] + repo: bool, + #[serde(default = "default_true")] + lang: bool, +} + pub(super) async fn handle( Query(mut api_params): Query, + Query(ac_params): Query, Extension(indexes): Extension>, ) -> Result { // Override page_size and set to low value @@ -43,11 +60,20 @@ pub(super) async fn handle( // If no flags completion, run a search with full query if autocomplete_results.is_empty() { - let contents = ContentReader.execute(&indexes.file, &queries, &api_params); - let repos = RepoReader.execute(&indexes.repo, &queries, &api_params); - let files = FileReader.execute(&indexes.file, &queries, &api_params); + let mut engines = vec![]; + if ac_params.content { + engines.push(ContentReader.execute(&indexes.file, &queries, &api_params)); + } + + if ac_params.file { + engines.push(RepoReader.execute(&indexes.repo, &queries, &api_params)); + } + + if ac_params.repo { + engines.push(FileReader.execute(&indexes.file, &queries, &api_params)); + } - let (langs, list) = stream::iter([contents, repos, files]) + let (langs, list) = stream::iter(engines) // Buffer several readers at the same time. The exact number is not important; this is // simply an upper bound. .buffered(10) @@ -72,9 +98,12 @@ pub(super) async fn handle( autocomplete_results.extend(list); - let mut ranked_langs = langs.into_iter().collect::>(); - ranked_langs.sort_by(|(_, a_count), (_, b_count)| a_count.cmp(b_count)); - autocomplete_results.extend(ranked_langs.into_iter().map(|(l, _)| QueryResult::Lang(l))); + if ac_params.lang { + let mut ranked_langs = langs.into_iter().collect::>(); + ranked_langs.sort_by(|(_, a_count), (_, b_count)| a_count.cmp(b_count)); + autocomplete_results + .extend(ranked_langs.into_iter().map(|(l, _)| QueryResult::Lang(l))); + } } Ok(json(AutocompleteResponse { From ae78fa9edf95081e3bd2aecfc0d159fc6de16eed Mon Sep 17 00:00:00 2001 From: rsdy Date: Mon, 13 Nov 2023 20:06:36 +0100 Subject: [PATCH 15/42] Normalize results somewhat --- server/bleep/src/query/parser.rs | 14 ++++++++--- server/bleep/src/webserver/autocomplete.rs | 29 +++++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/server/bleep/src/query/parser.rs b/server/bleep/src/query/parser.rs index 6459330c23..ab2b0edc85 100644 --- a/server/bleep/src/query/parser.rs +++ b/server/bleep/src/query/parser.rs @@ -1,7 +1,7 @@ use pest::{iterators::Pair, Parser}; use regex::Regex; use smallvec::{smallvec, SmallVec}; -use std::{borrow::Cow, collections::HashSet, mem}; +use std::{borrow::Cow, collections::HashSet, mem, ops::Deref}; #[derive(Default, Clone, Debug, PartialEq, Eq)] pub struct Query<'a> { @@ -167,6 +167,14 @@ impl<'a> Query<'a> { } impl<'a> Target<'a> { + /// Get the inner literal for this target, regardless of the variant. + pub fn literal_mut(&'a mut self) -> &mut Literal<'a> { + match self { + Self::Symbol(lit) => lit, + Self::Content(lit) => lit, + } + } + /// Get the inner literal for this target, regardless of the variant. pub fn literal(&self) -> &Literal<'_> { match self { @@ -254,7 +262,7 @@ impl<'a, T: AsRef> From for LiteralInner<'a> { } } -impl<'a> std::ops::Deref for LiteralInner<'a> { +impl<'a> Deref for LiteralInner<'a> { type Target = str; fn deref(&self) -> &Self::Target { @@ -388,7 +396,7 @@ impl<'a, 'b: 'a> AsRef> for Literal<'b> { } } -impl std::ops::Deref for Literal<'_> { +impl Deref for Literal<'_> { type Target = str; fn deref(&self) -> &Self::Target { diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index c3bd06ea04..8726d02036 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -42,7 +42,32 @@ pub(super) async fn handle( api_params.page = 0; api_params.page_size = 10; - let queries = parser::parse(&api_params.q).map_err(Error::user)?; + let queries = parser::parse(&api_params.q) + .map_err(Error::user)? + .into_iter() + .map(|mut q| { + let target = q.target.get_or_insert_with(|| Target::Content(" ".into())); + + for keyword in &["path:", "repo:"] { + if let Some(pos) = target.literal().find(keyword) { + let new = format!( + "{}{}", + &target.literal()[..pos], + &target.literal()[pos + keyword.len()..] + ); + + *target = Target::Content(Literal::Plain(if new.is_empty() { + " ".into() + } else { + new.into() + })); + } + } + + q.lang = None; + q + }) + .collect::>(); let mut autocomplete_results = vec![]; // Only execute prefix search on flag names if there is a non-regex content target. @@ -101,6 +126,8 @@ pub(super) async fn handle( if ac_params.lang { let mut ranked_langs = langs.into_iter().collect::>(); ranked_langs.sort_by(|(_, a_count), (_, b_count)| a_count.cmp(b_count)); + ranked_langs.reverse(); + ranked_langs.truncate(5); autocomplete_results .extend(ranked_langs.into_iter().map(|(l, _)| QueryResult::Lang(l))); } From 571ef8642670faa38b7a1fa3d9dcf5f9e71e7eb9 Mon Sep 17 00:00:00 2001 From: rsdy Date: Tue, 14 Nov 2023 13:31:33 +0100 Subject: [PATCH 16/42] Show lang filters more aggressively --- server/bleep/src/webserver/autocomplete.rs | 60 ++++++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 8726d02036..317504946d 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -8,7 +8,7 @@ use crate::{ }, query::{ execute::{ApiQuery, ExecuteQuery, QueryResult}, - parser, + languages, parser, parser::{Literal, Target}, }, }; @@ -42,29 +42,45 @@ pub(super) async fn handle( api_params.page = 0; api_params.page_size = 10; + let mut boost_langs = None; + let queries = parser::parse(&api_params.q) .map_err(Error::user)? .into_iter() .map(|mut q| { - let target = q.target.get_or_insert_with(|| Target::Content(" ".into())); - - for keyword in &["path:", "repo:"] { - if let Some(pos) = target.literal().find(keyword) { - let new = format!( - "{}{}", - &target.literal()[..pos], - &target.literal()[pos + keyword.len()..] - ); - - *target = Target::Content(Literal::Plain(if new.is_empty() { - " ".into() - } else { - new.into() - })); + if ac_params.content { + let target = q.target.get_or_insert_with(|| Target::Content(" ".into())); + + for keyword in &["path:", "repo:"] { + if let Some(pos) = target.literal().find(keyword) { + let new = format!( + "{}{}", + &target.literal()[..pos], + &target.literal()[pos + keyword.len()..] + ); + + *target = Target::Content(Literal::Plain(if new.is_empty() { + " ".into() + } else { + new.into() + })); + } + } + } else { + q.target = None; + } + + if let Some(lang) = q.lang.as_ref() { + if languages::list() + .filter(|l| l.to_lowercase() == lang.as_ref().to_lowercase()) + .count() + < 1 + { + q.lang = None; + boost_langs = q.lang.as_ref().map(|l| l.to_string()); } } - q.lang = None; q }) .collect::>(); @@ -128,6 +144,16 @@ pub(super) async fn handle( ranked_langs.sort_by(|(_, a_count), (_, b_count)| a_count.cmp(b_count)); ranked_langs.reverse(); ranked_langs.truncate(5); + + if let Some(partial) = boost_langs { + ranked_langs.extend( + languages::list() + .filter(|name| name.contains(&partial)) + .take(5 - ranked_langs.len()) + .map(|lang| (lang.to_lowercase(), 1)), + ); + } + autocomplete_results .extend(ranked_langs.into_iter().map(|(l, _)| QueryResult::Lang(l))); } From a5c03f7ca668a3d29ad7327861c8b211ab1b5139 Mon Sep 17 00:00:00 2001 From: rsdy Date: Tue, 14 Nov 2023 13:58:42 +0100 Subject: [PATCH 17/42] Add arbitrary language suggestions --- server/bleep/src/webserver/autocomplete.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 317504946d..29aa08ba31 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -159,6 +159,14 @@ pub(super) async fn handle( } } + if autocomplete_results.is_empty() && ac_params.lang { + autocomplete_results.extend( + languages::list() + .take(5) + .map(|l| QueryResult::Lang(l.into())), + ) + } + Ok(json(AutocompleteResponse { count: autocomplete_results.len(), data: autocomplete_results, From 4cd44a9ea93017c8b4f1367a97cc475fea4756a2 Mon Sep 17 00:00:00 2001 From: rsdy Date: Tue, 14 Nov 2023 14:20:24 +0100 Subject: [PATCH 18/42] Tuning --- server/bleep/src/webserver/autocomplete.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 29aa08ba31..08d969bb83 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -51,7 +51,7 @@ pub(super) async fn handle( if ac_params.content { let target = q.target.get_or_insert_with(|| Target::Content(" ".into())); - for keyword in &["path:", "repo:"] { + for keyword in &["lang:", "path:", "repo:"] { if let Some(pos) = target.literal().find(keyword) { let new = format!( "{}{}", @@ -81,6 +81,10 @@ pub(super) async fn handle( } } + if q.path.is_none() && ac_params.file { + q.path = Some(Literal::Regex(".".into())); + } + q }) .collect::>(); @@ -106,11 +110,11 @@ pub(super) async fn handle( engines.push(ContentReader.execute(&indexes.file, &queries, &api_params)); } - if ac_params.file { + if ac_params.repo { engines.push(RepoReader.execute(&indexes.repo, &queries, &api_params)); } - if ac_params.repo { + if ac_params.file { engines.push(FileReader.execute(&indexes.file, &queries, &api_params)); } @@ -159,14 +163,6 @@ pub(super) async fn handle( } } - if autocomplete_results.is_empty() && ac_params.lang { - autocomplete_results.extend( - languages::list() - .take(5) - .map(|l| QueryResult::Lang(l.into())), - ) - } - Ok(json(AutocompleteResponse { count: autocomplete_results.len(), data: autocomplete_results, From ba67c69d015ac04e205ff2e26e473866543b1d97 Mon Sep 17 00:00:00 2001 From: rsdy Date: Tue, 14 Nov 2023 14:29:44 +0100 Subject: [PATCH 19/42] Make it a greedy regex --- server/bleep/src/webserver/autocomplete.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/bleep/src/webserver/autocomplete.rs b/server/bleep/src/webserver/autocomplete.rs index 08d969bb83..ed9d145e28 100644 --- a/server/bleep/src/webserver/autocomplete.rs +++ b/server/bleep/src/webserver/autocomplete.rs @@ -82,7 +82,7 @@ pub(super) async fn handle( } if q.path.is_none() && ac_params.file { - q.path = Some(Literal::Regex(".".into())); + q.path = Some(Literal::Regex(".*".into())); } q From cec32c7067ce10b609fa782ceb67ee4e497f073e Mon Sep 17 00:00:00 2001 From: anastasiia Date: Fri, 10 Nov 2023 10:51:24 -0500 Subject: [PATCH 20/42] render parsed user query in history --- .../Chat/ChatBody/AllCoversations/index.tsx | 9 +- .../components/Chat/ChatBody/Conversation.tsx | 3 + .../UserParsedQuery/PathChip.tsx | 31 + .../UserParsedQuery/index.tsx | 29 + .../ChatBody/ConversationMessage/index.tsx | 16 +- client/src/locales/en.json | 3 +- client/src/locales/es.json | 3 +- client/src/locales/it.json | 761 +++++++++--------- client/src/locales/ja.json | 3 +- client/src/locales/zh-CN.json | 3 +- client/src/mappers/conversation.ts | 59 +- client/src/types/api.ts | 29 +- client/src/types/general.ts | 9 + 13 files changed, 566 insertions(+), 392 deletions(-) create mode 100644 client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/PathChip.tsx create mode 100644 client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/index.tsx diff --git a/client/src/components/Chat/ChatBody/AllCoversations/index.tsx b/client/src/components/Chat/ChatBody/AllCoversations/index.tsx index 4924683d49..3f9aa9bd89 100644 --- a/client/src/components/Chat/ChatBody/AllCoversations/index.tsx +++ b/client/src/components/Chat/ChatBody/AllCoversations/index.tsx @@ -21,7 +21,10 @@ import { OpenChatHistoryItem, } from '../../../../types/general'; import { conversationsCache } from '../../../../services/cache'; -import { mapLoadingSteps } from '../../../../mappers/conversation'; +import { + mapLoadingSteps, + mapUserQuery, +} from '../../../../mappers/conversation'; import { LocaleContext } from '../../../../context/localeContext'; import { getDateFnsLocale } from '../../../../utils'; import ConversationListItem from './ConversationListItem'; @@ -63,9 +66,11 @@ const AllConversations = ({ resp.forEach((m) => { // @ts-ignore const userQuery = m.search_steps.find((s) => s.type === 'QUERY'); + const parsedQuery = mapUserQuery(m); conv.push({ author: ChatMessageAuthor.User, - text: m.query?.target?.Plain || userQuery?.content?.query || '', + text: m.raw_query || userQuery?.content?.query || '', + parsedQuery, isFromHistory: true, }); conv.push({ diff --git a/client/src/components/Chat/ChatBody/Conversation.tsx b/client/src/components/Chat/ChatBody/Conversation.tsx index 6346ab00b3..314d43f795 100644 --- a/client/src/components/Chat/ChatBody/Conversation.tsx +++ b/client/src/components/Chat/ChatBody/Conversation.tsx @@ -59,6 +59,9 @@ const Conversation = ({ isHistory={isHistory} author={m.author} message={m.text} + parsedQuery={ + m.author === ChatMessageAuthor.Server ? undefined : m.parsedQuery + } error={m.author === ChatMessageAuthor.Server ? m.error : ''} showInlineFeedback={ m.author === ChatMessageAuthor.Server && diff --git a/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/PathChip.tsx b/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/PathChip.tsx new file mode 100644 index 0000000000..a20f299c07 --- /dev/null +++ b/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/PathChip.tsx @@ -0,0 +1,31 @@ +import { useMemo } from 'react'; +import { FolderClosed, ArrowOut } from '../../../../../icons'; +import FileIcon from '../../../../FileIcon'; +import { splitPath } from '../../../../../utils'; + +type Props = { + path: string; +}; + +const PathChip = ({ path }: Props) => { + const isFolder = useMemo(() => path.endsWith('/'), [path]); + return ( + + + {isFolder ? ( + + ) : ( + + )} + + {isFolder ? path.replace(/\/$/, '') : splitPath(path).pop()} + + + + ); +}; + +export default PathChip; diff --git a/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/index.tsx b/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/index.tsx new file mode 100644 index 0000000000..16a7da613a --- /dev/null +++ b/client/src/components/Chat/ChatBody/ConversationMessage/UserParsedQuery/index.tsx @@ -0,0 +1,29 @@ +import { memo } from 'react'; +import { + ParsedQueryType, + ParsedQueryTypeEnum, +} from '../../../../../types/general'; +import PathChip from './PathChip'; + +type Props = { + textQuery: string; + parsedQuery?: ParsedQueryType[]; +}; + +const UserParsedQuery = ({ textQuery, parsedQuery }: Props) => { + return ( +
+ {parsedQuery + ? parsedQuery.map((p, i) => + p.type === ParsedQueryTypeEnum.TEXT ? ( + p.text + ) : p.type === ParsedQueryTypeEnum.PATH ? ( + + ) : null, + ) + : textQuery} +
+ ); +}; + +export default memo(UserParsedQuery); diff --git a/client/src/components/Chat/ChatBody/ConversationMessage/index.tsx b/client/src/components/Chat/ChatBody/ConversationMessage/index.tsx index af21e0a507..eb34863e50 100644 --- a/client/src/components/Chat/ChatBody/ConversationMessage/index.tsx +++ b/client/src/components/Chat/ChatBody/ConversationMessage/index.tsx @@ -10,7 +10,11 @@ import { WrenchAndScrewdriver, } from '../../../../icons'; import { DeviceContext } from '../../../../context/deviceContext'; -import { ChatLoadingStep, ChatMessageAuthor } from '../../../../types/general'; +import { + ChatLoadingStep, + ChatMessageAuthor, + ParsedQueryType, +} from '../../../../types/general'; import { ChatContext } from '../../../../context/chatContext'; import Button from '../../../Button'; import { LocaleContext } from '../../../../context/localeContext'; @@ -24,10 +28,12 @@ import { } from '../../../../services/storage'; import MessageFeedback from './MessageFeedback'; import FileChip from './FileChip'; +import UserParsedQuery from './UserParsedQuery'; type Props = { author: ChatMessageAuthor; message?: string; + parsedQuery?: ParsedQueryType[]; error?: string; threadId: string; queryId: string; @@ -61,6 +67,7 @@ const ConversationMessage = ({ onMessageEdit, responseTimestamp, singleFileExplanation, + parsedQuery, }: Props) => { const { t } = useTranslation(); const [isLoadingStepsShown, setLoadingStepsShown] = useState( @@ -172,7 +179,7 @@ const ConversationMessage = ({ )} - {message && ( + {!!message && (
{author === ChatMessageAuthor.Server ? ( ) : ( <> -
{message}
+ {!isHistory && !!queryId && (