diff --git a/client/web/src/integration/search-aggregation.test.ts b/client/web/src/integration/search-aggregation.test.ts index f816bea784b..f5085bb4839 100644 --- a/client/web/src/integration/search-aggregation.test.ts +++ b/client/web/src/integration/search-aggregation.test.ts @@ -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(() => { diff --git a/client/web/src/search/index.test.ts b/client/web/src/search/index.test.ts index 73134cc08e8..0a55323603f 100644 --- a/client/web/src/search/index.test.ts +++ b/client/web/src/search/index.test.ts @@ -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', () => { diff --git a/client/web/src/search/index.ts b/client/web/src/search/index.ts index 46a81f31051..3420941029f 100644 --- a/client/web/src/search/index.ts +++ b/client/web/src/search/index.ts @@ -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, diff --git a/client/web/src/stores/navbarSearchQueryState.test.ts b/client/web/src/stores/navbarSearchQueryState.test.ts index 722c523d2f7..6073f5626ad 100644 --- a/client/web/src/stores/navbarSearchQueryState.test.ts +++ b/client/web/src/stores/navbarSearchQueryState.test.ts @@ -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', () => { diff --git a/client/web/src/stores/navbarSearchQueryState.ts b/client/web/src/stores/navbarSearchQueryState.ts index 868254cb1cb..f01a8405989 100644 --- a/client/web/src/stores/navbarSearchQueryState.ts +++ b/client/web/src/stores/navbarSearchQueryState.ts @@ -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((set, get) => ({ parametersSource: InitialParametersSource.DEFAULT, queryState: { query: '' }, @@ -68,7 +74,10 @@ export const useNavbarQueryState = create((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