Search: surface pattern type in query input (#63326)

We plan to remove the 'Keyword Search' toggle as part of bringing the feature
to GA. Once the toggle is removed, the search UX will only represent `keyword`
(the default pattern type) and `regexp` (through the regex toggle), with no
visual indication for other pattern types. So if a user clicks on a link using
`patterntype:standard`, the search will just behave differently, without any
indication in the UX as to why.

This PR surfaces the `patterntype` filter in the search bar whenever it's not
`keyword` or `regexp`. That way, users can see an old pattern type is being
used and understand why the search behavior may be different.

Relates to SPLF-68
This commit is contained in:
Julie Tibshirani 2024-06-19 14:24:10 -07:00 committed by GitHub
parent 15ea951e0d
commit 5630eef9e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 37 additions and 83 deletions

View File

@ -307,7 +307,7 @@ describe('Search aggregation', () => {
const origQuery = 'context:global insights('
await driver.page.goto(
`${driver.sourcegraphBaseUrl}/search?q=${encodeURIComponent(origQuery)}&patternType=literal`
`${driver.sourcegraphBaseUrl}/search?q=${encodeURIComponent(origQuery)}&patternType=keyword`
)
await driver.page.evaluate(() => {

View File

@ -138,29 +138,6 @@ describe('search/index', () => {
searchMode: SearchMode.Precise,
})
})
test('parseSearchURL preserves literal search compatibility', () => {
expect(parseSearchURL('q=/a literal pattern/&patternType=literal')).toStrictEqual({
query: 'content:"/a literal pattern/"',
patternType: SearchPatternType.standard,
caseSensitive: false,
searchMode: SearchMode.Precise,
})
expect(parseSearchURL('q=not /a literal pattern/&patternType=literal')).toStrictEqual({
query: 'not content:"/a literal pattern/"',
patternType: SearchPatternType.standard,
caseSensitive: false,
searchMode: SearchMode.Precise,
})
expect(parseSearchURL('q=un.*touched&patternType=literal')).toStrictEqual({
query: 'un.*touched',
patternType: SearchPatternType.standard,
caseSensitive: false,
searchMode: SearchMode.Precise,
})
})
})
describe('repoFilterForRepoRevision escapes values with spaces', () => {

View File

@ -7,10 +7,7 @@ import { memoizeObservable } from '@sourcegraph/common'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import { SearchMode } from '@sourcegraph/shared/src/search'
import { discreteValueAliases, escapeSpaces } from '@sourcegraph/shared/src/search/query/filters'
import { stringHuman } from '@sourcegraph/shared/src/search/query/printer'
import { findFilter, FilterKind, getGlobalSearchContextFilter } from '@sourcegraph/shared/src/search/query/query'
import { scanSearchQuery } from '@sourcegraph/shared/src/search/query/scanner'
import { createLiteral } from '@sourcegraph/shared/src/search/query/token'
import { omitFilter } from '@sourcegraph/shared/src/search/query/transformer'
import type { AggregateStreamingSearchResults, StreamSearchOptions } from '@sourcegraph/shared/src/search/stream'
@ -100,25 +97,18 @@ export function parseSearchURL(
urlSearchQuery: string,
{ appendCaseFilter = false }: { appendCaseFilter?: boolean } = {}
): ParsedSearchURL {
let queryInput = parseSearchURLQuery(urlSearchQuery) || ''
let patternTypeInput = parseSearchURLPatternType(urlSearchQuery)
let query = parseSearchURLQuery(urlSearchQuery) || ''
let patternType = parseSearchURLPatternType(urlSearchQuery)
let caseSensitive = searchURLIsCaseSensitive(urlSearchQuery)
const searchMode = parseSearchURLSearchMode(urlSearchQuery)
const globalPatternType = findFilter(queryInput, 'patterntype', FilterKind.Global)
const globalPatternType = findFilter(query, 'patterntype', FilterKind.Global)
if (globalPatternType?.value && globalPatternType.value.type === 'literal') {
// Any `patterntype:` filter in the query should override the patternType= URL query parameter if it exists.
queryInput = omitFilter(queryInput, globalPatternType)
patternTypeInput = globalPatternType.value.value as SearchPatternType
query = omitFilter(query, globalPatternType)
patternType = globalPatternType.value.value as SearchPatternType
}
let query = queryInput
const { queryInput: newQuery, patternTypeInput: patternType } = literalSearchCompatibility({
queryInput,
patternTypeInput,
})
query = newQuery
const globalCase = findFilter(query, 'case', FilterKind.Global)
if (globalCase?.value && globalCase.value.type === 'literal') {
// Any `case:` filter in the query should override the case= URL query parameter if it exists.
@ -166,45 +156,6 @@ export function quoteIfNeeded(string: string): string {
return string
}
interface QueryCompatibility {
queryInput: string
patternTypeInput?: SearchPatternType
}
export function literalSearchCompatibility({ queryInput, patternTypeInput }: QueryCompatibility): QueryCompatibility {
if (patternTypeInput === undefined || patternTypeInput !== SearchPatternType.literal) {
return { queryInput, patternTypeInput }
}
const tokens = scanSearchQuery(queryInput, false, SearchPatternType.standard)
if (tokens.type === 'error') {
return { queryInput, patternTypeInput }
}
if (!tokens.term.find(token => token.type === 'pattern' && token.delimited)) {
// If no /.../ pattern exists in this literal search, just return the query as-is.
return { queryInput, patternTypeInput: SearchPatternType.standard }
}
const newQueryInput = stringHuman(
tokens.term.map(token =>
token.type === 'pattern' && token.delimited
? {
type: 'filter',
range: { start: 0, end: 0 },
field: createLiteral('content', { start: 0, end: 0 }, false),
value: createLiteral(`/${token.value}/`, { start: 0, end: 0 }, true),
negated: false /** if `NOT` was used on this pattern, it's already preserved */,
}
: token
)
)
return {
queryInput: newQueryInput,
patternTypeInput: SearchPatternType.standard,
}
}
export interface SearchStreamingProps {
streamSearch: (
queryObservable: Observable<string>,

View File

@ -95,6 +95,16 @@ describe('navbar query state', () => {
expect(useNavbarQueryState.getState().searchPatternType).toBe(SearchPatternType.keyword)
})
it('should add patterntype to query if not keyword or regexp', () => {
setQueryStateFromURL(parseSearchURL('q=hello!&patternType=keyword'))
expect(useNavbarQueryState.getState().queryState.query).toBe('hello!')
expect(useNavbarQueryState.getState().searchPatternType).toBe(SearchPatternType.keyword)
setQueryStateFromURL(parseSearchURL('q=hello!!&patternType=standard'))
expect(useNavbarQueryState.getState().queryState.query).toBe('hello!! patterntype:standard')
expect(useNavbarQueryState.getState().searchPatternType).toBe(SearchPatternType.standard)
})
})
describe('state initialization precedence', () => {

View File

@ -8,11 +8,12 @@ import create from 'zustand'
import {
type BuildSearchQueryURLParameters,
canSubmitSearch,
type SearchQueryState,
updateQuery,
InitialParametersSource,
SearchMode,
type SearchQueryState,
updateQuery,
} from '@sourcegraph/shared/src/search'
import { FilterType } from '@sourcegraph/shared/src/search/query/filters'
import type { Settings, SettingsCascadeOrError } from '@sourcegraph/shared/src/settings/settings'
import { buildSearchURLQuery } from '@sourcegraph/shared/src/util/url'
@ -27,6 +28,11 @@ import {
export interface NavbarQueryState extends SearchQueryState {}
const explicitPatternTypes = new Set([
SearchPatternType.keyword,
SearchPatternType.regexp,
SearchPatternType.structural,
])
export const useNavbarQueryState = create<NavbarQueryState>((set, get) => ({
parametersSource: InitialParametersSource.DEFAULT,
queryState: { query: '' },
@ -68,7 +74,10 @@ export const useNavbarQueryState = create<NavbarQueryState>((set, get) => ({
}))
export function setSearchPatternType(searchPatternType: SearchPatternType): void {
useNavbarQueryState.setState({ searchPatternType })
// When changing the patterntype, we also need to reset the query to strip out any potential patterntype: filter
const state = useNavbarQueryState.getState()
const query = state.searchQueryFromURL ?? state.queryState.query
useNavbarQueryState.setState({ searchPatternType, queryState: { query } })
}
export function setSearchCaseSensitivity(searchCaseSensitivity: boolean): void {
@ -108,8 +117,15 @@ export function setQueryStateFromURL(parsedSearchURL: ParsedSearchURL, query = p
// Only update flags if the URL contains a search query.
newState.parametersSource = InitialParametersSource.URL
newState.searchCaseSensitivity = parsedSearchURL.caseSensitive
if (parsedSearchURL.patternType !== undefined) {
newState.searchPatternType = parsedSearchURL.patternType
const parsedPatternType = parsedSearchURL.patternType
if (parsedPatternType !== undefined) {
newState.searchPatternType = parsedPatternType
// Only keyword, regexp, and structural are represented in the UI. For other pattern types, we make
// sure to surface them in the query input itself.
if (!explicitPatternTypes.has(parsedPatternType)) {
query = `${query} ${FilterType.patterntype}:${parsedPatternType}`
}
}
newState.queryState = { query }
newState.searchQueryFromURL = parsedSearchURL.query