Search: Add initial keyword search toggle (#59779)

This change adds a simple toggle to enable/ disable keyword search, and removes
the prototype toggle called "Precise (NEW)". It also removes the smart search
toggle, since smart search doesn't pair well with the new keyword search
behavior. When the feature is disabled, we revert to the old UI and defaults
(smart search is still present, and we default to the 'standard' patterntype).
This helps mitigate risk for the initial release, since we can always disable
the change completely.

The toggle behaves like other toggles in the nav bar: the setting sticks unless
the user opens a new tab or refreshes the page. Then it reverts back,
defaulting to "keyword search".
This commit is contained in:
Julie Tibshirani 2024-01-24 11:26:18 -08:00 committed by GitHub
parent 84235b58d8
commit cfa1faeefc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 415 additions and 335 deletions

View File

@ -116,9 +116,9 @@ ts_project(
"src/search-ui/input/experimental/placeholder.ts",
"src/search-ui/input/experimental/suggestionsExtension.ts",
"src/search-ui/input/experimental/utils.ts",
"src/search-ui/input/toggles/LegacyToggles.tsx",
"src/search-ui/input/toggles/QueryInputToggle.tsx",
"src/search-ui/input/toggles/SmartSearchToggle.tsx",
"src/search-ui/input/toggles/SmartSearchToggleExtended.tsx",
"src/search-ui/input/toggles/Toggles.tsx",
"src/search-ui/input/toggles/index.ts",
"src/search-ui/results/AnnotatedSearchExample.tsx",

View File

@ -13,6 +13,7 @@ import {
import { getGlobalSearchContextFilter } from '@sourcegraph/shared/src/search/query/query'
import { omitFilter } from '@sourcegraph/shared/src/search/query/transformer'
import type { fetchStreamSuggestions as defaultFetchStreamSuggestions } from '@sourcegraph/shared/src/search/suggestions'
import { useExperimentalFeatures } from '@sourcegraph/shared/src/settings/settings'
import type { RecentSearch } from '@sourcegraph/shared/src/settings/temporary/recentSearches'
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary/useTemporarySetting'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
@ -22,7 +23,8 @@ import { SearchButton } from './SearchButton'
import { SearchContextDropdown } from './SearchContextDropdown'
import { SearchHelpDropdownButton } from './SearchHelpDropdownButton'
import { SearchHistoryDropdown } from './SearchHistoryDropdown'
import { Toggles, type TogglesProps } from './toggles'
import { LegacyToggles } from './toggles'
import { Toggles, type TogglesProps } from './toggles/Toggles'
import styles from './SearchBox.module.scss'
@ -117,6 +119,8 @@ export const SearchBox: FC<SearchBoxProps> = props => {
})
}, [recentSearches, selectedSearchContextSpec])
const showKeywordSearchToggle = useExperimentalFeatures(features => features.keywordSearch)
return (
<div
className={classNames(
@ -185,19 +189,33 @@ export const SearchBox: FC<SearchBoxProps> = props => {
searchHistory={recentSearchesWithoutSearchContext}
onSelectSearchFromHistory={onInlineSearchHistorySelect}
/>
<Toggles
patternType={props.patternType}
setPatternType={props.setPatternType}
caseSensitive={props.caseSensitive}
setCaseSensitivity={props.setCaseSensitivity}
searchMode={props.searchMode}
setSearchMode={props.setSearchMode}
submitSearch={props.submitSearchOnToggle}
navbarSearchQuery={queryState.query}
className={styles.searchBoxToggles}
structuralSearchDisabled={props.structuralSearchDisabled}
showExtendedPicker={props.showExtendedPicker}
/>
{showKeywordSearchToggle ? (
<Toggles
patternType={props.patternType}
setPatternType={props.setPatternType}
caseSensitive={props.caseSensitive}
setCaseSensitivity={props.setCaseSensitivity}
searchMode={props.searchMode}
setSearchMode={props.setSearchMode}
submitSearch={props.submitSearchOnToggle}
navbarSearchQuery={queryState.query}
className={styles.searchBoxToggles}
structuralSearchDisabled={props.structuralSearchDisabled}
/>
) : (
<LegacyToggles
patternType={props.patternType}
setPatternType={props.setPatternType}
caseSensitive={props.caseSensitive}
setCaseSensitivity={props.setCaseSensitivity}
searchMode={props.searchMode}
setSearchMode={props.setSearchMode}
submitSearch={props.submitSearchOnToggle}
navbarSearchQuery={queryState.query}
className={styles.searchBoxToggles}
structuralSearchDisabled={props.structuralSearchDisabled}
/>
)}
</div>
</div>
<div className={styles.searchBoxButton}>

View File

@ -0,0 +1,186 @@
import React, { useCallback } from 'react'
import { mdiCodeBrackets, mdiFormatLetterCase, mdiRegex } from '@mdi/js'
import classNames from 'classnames'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import {
type CaseSensitivityProps,
type SearchPatternTypeMutationProps,
type SubmitSearchProps,
SearchMode,
type SearchModeProps,
type SearchPatternTypeProps,
} from '@sourcegraph/shared/src/search'
import { findFilter, FilterKind } from '@sourcegraph/shared/src/search/query/query'
import { QueryInputToggle } from './QueryInputToggle'
import { SmartSearchToggle } from './SmartSearchToggle'
import styles from './Toggles.module.scss'
export interface LegacyTogglesProps
extends SearchPatternTypeProps,
SearchPatternTypeMutationProps,
CaseSensitivityProps,
SearchModeProps,
Partial<Pick<SubmitSearchProps, 'submitSearch'>> {
navbarSearchQuery: string
className?: string
showSmartSearchButton?: boolean
/**
* If set to false makes all buttons non-actionable. The main use case for
* this prop is showing the toggles in examples. This is different from
* being disabled, because the buttons still render normally.
*/
interactive?: boolean
/** Comes from JSContext only set in the web app. */
structuralSearchDisabled?: boolean
}
/**
* The toggles displayed in the query input.
*
* @deprecated This component is only used when the 'keyword search' language update
* is disabled, and will be removed in a follow-up release.
*/
export const LegacyToggles: React.FunctionComponent<React.PropsWithChildren<LegacyTogglesProps>> = (
props: LegacyTogglesProps
) => {
const {
navbarSearchQuery,
patternType,
setPatternType,
caseSensitive,
setCaseSensitivity,
searchMode,
setSearchMode,
className,
submitSearch,
showSmartSearchButton = true,
structuralSearchDisabled,
} = props
const submitOnToggle = useCallback(
(
args:
| { newPatternType: SearchPatternType }
| { newCaseSensitivity: boolean }
| { newPowerUser: boolean }
| { newSearchMode: SearchMode }
): void => {
submitSearch?.({
source: 'filter',
patternType: 'newPatternType' in args ? args.newPatternType : patternType,
caseSensitive: 'newCaseSensitivity' in args ? args.newCaseSensitivity : caseSensitive,
searchMode: 'newSearchMode' in args ? args.newSearchMode : searchMode,
})
},
[caseSensitive, patternType, searchMode, submitSearch]
)
const toggleCaseSensitivity = useCallback((): void => {
const newCaseSensitivity = !caseSensitive
setCaseSensitivity(newCaseSensitivity)
submitOnToggle({ newCaseSensitivity })
}, [caseSensitive, setCaseSensitivity, submitOnToggle])
const toggleRegexp = useCallback((): void => {
const newPatternType =
patternType !== SearchPatternType.regexp ? SearchPatternType.regexp : SearchPatternType.standard
setPatternType(newPatternType)
submitOnToggle({ newPatternType })
}, [patternType, setPatternType, submitOnToggle])
const toggleStructuralSearch = useCallback((): void => {
const newPatternType: SearchPatternType =
patternType !== SearchPatternType.structural ? SearchPatternType.structural : SearchPatternType.standard
setPatternType(newPatternType)
submitOnToggle({ newPatternType })
}, [patternType, setPatternType, submitOnToggle])
const onSelectSmartSearch = useCallback(
(enabled: boolean): void => {
const newSearchMode: SearchMode = enabled ? SearchMode.SmartSearch : SearchMode.Precise
setSearchMode(newSearchMode)
submitOnToggle({ newSearchMode })
},
[setSearchMode, submitOnToggle]
)
return (
<div className={classNames(className, styles.toggleContainer)}>
<>
<QueryInputToggle
title="Case sensitivity"
isActive={caseSensitive}
onToggle={toggleCaseSensitivity}
iconSvgPath={mdiFormatLetterCase}
interactive={props.interactive}
className={`test-case-sensitivity-toggle ${styles.caseSensitivityToggle}`}
disableOn={[
{
condition: findFilter(navbarSearchQuery, 'case', FilterKind.Subexpression) !== undefined,
reason: 'Query already contains one or more case subexpressions',
},
{
condition:
findFilter(navbarSearchQuery, 'patterntype', FilterKind.Subexpression) !== undefined,
reason: 'Query contains one or more patterntype subexpressions, cannot apply global case-sensitivity',
},
{
condition: patternType === SearchPatternType.structural,
reason: 'Structural search is always case sensitive',
},
]}
/>
<QueryInputToggle
title="Regular expression"
isActive={patternType === SearchPatternType.regexp}
onToggle={toggleRegexp}
iconSvgPath={mdiRegex}
interactive={props.interactive}
className={`test-regexp-toggle ${styles.regularExpressionToggle}`}
disableOn={[
{
condition:
findFilter(navbarSearchQuery, 'patterntype', FilterKind.Subexpression) !== undefined,
reason: 'Query already contains one or more patterntype subexpressions',
},
]}
/>
<>
{!structuralSearchDisabled && (
<QueryInputToggle
title="Structural search"
className={`test-structural-search-toggle ${styles.structuralSearchToggle}`}
isActive={patternType === SearchPatternType.structural}
onToggle={toggleStructuralSearch}
iconSvgPath={mdiCodeBrackets}
interactive={props.interactive}
disableOn={[
{
condition:
findFilter(navbarSearchQuery, 'patterntype', FilterKind.Subexpression) !==
undefined,
reason: 'Query already contains one or more patterntype subexpressions',
},
]}
/>
)}
</>
{showSmartSearchButton && <div className={styles.separator} />}
{showSmartSearchButton && (
<SmartSearchToggle
className="test-smart-search-toggle"
isActive={searchMode === SearchMode.SmartSearch}
onSelect={onSelectSmartSearch}
interactive={props.interactive}
/>
)}
</>
</div>
)
}

View File

@ -1,171 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react'
import { mdiClose, mdiRadioboxBlank, mdiRadioboxMarked, mdiHeart } from '@mdi/js'
import classNames from 'classnames'
import {
Button,
Icon,
Input,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Position,
Tooltip,
H4,
H2,
} from '@sourcegraph/wildcard'
import type { ToggleProps } from './QueryInputToggle'
import smartStyles from './SmartSearchToggle.module.scss'
import styles from './Toggles.module.scss'
export const smartSearchIconSvgPath =
'M11.3956 20H10.2961L11.3956 13.7778H7.54754C6.58003 13.7778 7.18473 13.1111 7.20671 13.0844C8.62499 11.0578 10.7579 8.03556 13.6054 4H14.7049L13.6054 10.2222H17.4645C17.9042 10.2222 18.1461 10.3911 17.9042 10.8089C13.5615 16.9333 11.3956 20 11.3956 20Z'
export enum SearchModes {
Smart = 'Smart',
PreciseNew = 'Precise (NEW) 💖',
Precise = 'Precise (legacy)',
}
interface SmartSearchToggleProps extends Omit<ToggleProps, 'title' | 'iconSvgPath' | 'onToggle' | 'isActive'> {
onSelect: (mode: SearchModes) => void
mode: SearchModes
}
/**
* A toggle displayed in the QueryInput.
*/
export const SmartSearchToggleExtended: React.FunctionComponent<SmartSearchToggleProps> = ({
onSelect,
interactive = true,
mode,
className,
}) => {
const tooltipValue = mode.toString()
const interactiveProps = interactive ? {} : { tabIndex: -1, 'aria-hidden': true }
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
return (
<Popover isOpen={isPopoverOpen} onOpenChange={event => setIsPopoverOpen(event.isOpen)}>
<Tooltip content={tooltipValue} placement="bottom">
<PopoverTrigger
as={Button}
className={classNames(
'a11y-ignore',
styles.toggle,
smartStyles.button,
className,
!interactive && styles.toggleNonInteractive,
mode !== SearchModes.Precise && styles.toggleActive
)}
variant="icon"
{...interactiveProps}
>
<Icon
aria-label={tooltipValue}
svgPath={mode === SearchModes.PreciseNew ? mdiHeart : smartSearchIconSvgPath}
// compensate for left margin set on "svg" for the flash symbol
className={mode === SearchModes.PreciseNew ? 'ml-0' : ''}
/>
</PopoverTrigger>
</Tooltip>
<SmartSearchToggleMenu onSelect={onSelect} mode={mode} closeMenu={() => setIsPopoverOpen(false)} />
</Popover>
)
}
const SmartSearchToggleMenu: React.FunctionComponent<
Pick<SmartSearchToggleProps, 'onSelect' | 'mode'> & { closeMenu: () => void }
> = ({ onSelect, mode, closeMenu }) => {
const [getMode, setMode] = useState(mode)
useEffect(() => {
setMode(mode)
}, [mode])
const onChange = useCallback(
(value: SearchModes) => {
setMode(value)
// Wait a tiny bit for user to see the selection change before closing the popover
setTimeout(() => {
onSelect(value)
closeMenu()
}, 100)
},
[onSelect, closeMenu]
)
return (
<PopoverContent
aria-labelledby="smart-search-popover-header"
position={Position.bottomEnd}
className={smartStyles.popoverWindow}
>
<div className="d-flex justify-content-end px-3 py-2">
<H4 as={H2} id="smart-search-popover-header" className="m-0 flex-1">
Search Mode Picker
</H4>
<Button onClick={() => closeMenu()} variant="icon" aria-label="Close">
<Icon aria-hidden={true} svgPath={mdiClose} />
</Button>
</div>
<RadioItem
value={SearchModes.Smart}
header="Smart"
description="Suggest variations of your query to find more results that may relate."
isChecked={getMode === SearchModes.Smart}
onSelect={onChange}
/>
<RadioItem
value={SearchModes.PreciseNew}
header="Precise (NEW) 💖"
description="Spaces are interpreted as AND."
isChecked={getMode === SearchModes.PreciseNew}
onSelect={onChange}
/>
<RadioItem
value={SearchModes.Precise}
header="Precise (legacy)"
description="Spaces are interpreted literaly."
isChecked={getMode === SearchModes.Precise}
onSelect={onChange}
/>
</PopoverContent>
)
}
const RadioItem: React.FunctionComponent<{
value: SearchModes
isChecked: boolean
onSelect: (value: SearchModes) => void
header: string
description: string
}> = ({ value, isChecked, onSelect, header, description }) => (
<Label className={smartStyles.label}>
<Input
className="sr-only"
type="radio"
name="smartSearch"
value={value.toString()}
checked={isChecked}
onChange={() => onSelect(value)}
/>
<Icon
svgPath={isChecked ? mdiRadioboxMarked : mdiRadioboxBlank}
aria-hidden={true}
className={classNames(smartStyles.radioIcon, isChecked && smartStyles.radioIconActive)}
inline={false}
/>
<span className="d-flex flex-column">
<span className={smartStyles.radioHeader}>{header}</span>
<span className={smartStyles.radioDescription}>{description}</span>
</span>
</Label>
)

View File

@ -8,15 +8,12 @@ import {
type CaseSensitivityProps,
type SearchPatternTypeMutationProps,
type SubmitSearchProps,
SearchMode,
type SearchModeProps,
type SearchPatternTypeProps,
} from '@sourcegraph/shared/src/search'
import { findFilter, FilterKind } from '@sourcegraph/shared/src/search/query/query'
import { QueryInputToggle } from './QueryInputToggle'
import { SmartSearchToggle } from './SmartSearchToggle'
import { SmartSearchToggleExtended, SearchModes } from './SmartSearchToggleExtended'
import styles from './Toggles.module.scss'
@ -28,12 +25,6 @@ export interface TogglesProps
Partial<Pick<SubmitSearchProps, 'submitSearch'>> {
navbarSearchQuery: string
className?: string
showSmartSearchButton?: boolean
/**
* If set to true, the search mode picker will let the user select the new
* pattern type as a new alternative
*/
showExtendedPicker?: boolean
/**
* If set to false makes all buttons non-actionable. The main use case for
* this prop is showing the toggles in examples. This is different from
@ -54,31 +45,20 @@ export const Toggles: React.FunctionComponent<React.PropsWithChildren<TogglesPro
setPatternType,
caseSensitive,
setCaseSensitivity,
searchMode,
setSearchMode,
className,
submitSearch,
showSmartSearchButton = true,
showExtendedPicker = false,
structuralSearchDisabled,
} = props
const submitOnToggle = useCallback(
(
args:
| { newPatternType: SearchPatternType }
| { newCaseSensitivity: boolean }
| { newPowerUser: boolean }
| { newSearchMode: SearchMode }
): void => {
(args: { newPatternType: SearchPatternType } | { newCaseSensitivity: boolean }): void => {
submitSearch?.({
source: 'filter',
patternType: 'newPatternType' in args ? args.newPatternType : patternType,
caseSensitive: 'newCaseSensitivity' in args ? args.newCaseSensitivity : caseSensitive,
searchMode: 'newSearchMode' in args ? args.newSearchMode : searchMode,
})
},
[caseSensitive, patternType, searchMode, submitSearch]
[caseSensitive, patternType, submitSearch]
)
const toggleCaseSensitivity = useCallback((): void => {
@ -89,62 +69,20 @@ export const Toggles: React.FunctionComponent<React.PropsWithChildren<TogglesPro
const toggleRegexp = useCallback((): void => {
const newPatternType =
patternType !== SearchPatternType.regexp ? SearchPatternType.regexp : SearchPatternType.standard
patternType !== SearchPatternType.regexp ? SearchPatternType.regexp : SearchPatternType.keyword
setPatternType(newPatternType)
submitOnToggle({ newPatternType })
}, [patternType, setPatternType, submitOnToggle])
const toggleKeyword = useCallback((): void => {
const newPatternType =
patternType !== SearchPatternType.keyword ? SearchPatternType.keyword : SearchPatternType.standard
setPatternType(newPatternType)
// We always want precise mode when switching to the experimental pattern type.
setSearchMode(SearchMode.Precise)
submitOnToggle({ newPatternType })
}, [patternType, setPatternType, submitOnToggle, setSearchMode])
const toggleStructuralSearch = useCallback((): void => {
const newPatternType: SearchPatternType =
patternType !== SearchPatternType.structural ? SearchPatternType.structural : SearchPatternType.standard
patternType !== SearchPatternType.structural ? SearchPatternType.structural : SearchPatternType.keyword
setPatternType(newPatternType)
submitOnToggle({ newPatternType })
}, [patternType, setPatternType, submitOnToggle])
const onSelectSmartSearch = useCallback(
(enabled: boolean): void => {
const newSearchMode: SearchMode = enabled ? SearchMode.SmartSearch : SearchMode.Precise
// Disable the experimental pattern type the user activates smart search
if (patternType === SearchPatternType.keyword) {
setPatternType(SearchPatternType.standard)
}
setSearchMode(newSearchMode)
submitOnToggle({ newSearchMode })
},
[setSearchMode, submitOnToggle, patternType, setPatternType]
)
// This is hacky and is just for demo purposes. Once we have made the new
// pattern type the default we can revert this.
const onSelectSearchMode = useCallback(
(mode: SearchModes): void => {
if (mode === SearchModes.Smart) {
onSelectSmartSearch(true)
} else if (mode === SearchModes.PreciseNew) {
toggleKeyword()
} else {
onSelectSmartSearch(false)
}
},
[onSelectSmartSearch, toggleKeyword]
)
return (
<div className={classNames(className, styles.toggleContainer)}>
<>
@ -206,29 +144,6 @@ export const Toggles: React.FunctionComponent<React.PropsWithChildren<TogglesPro
/>
)}
</>
{showSmartSearchButton && <div className={styles.separator} />}
{showSmartSearchButton &&
(showExtendedPicker ? (
<SmartSearchToggleExtended
className="test-smart-search-toggle"
mode={
patternType === SearchPatternType.keyword
? SearchModes.PreciseNew
: searchMode === SearchMode.SmartSearch
? SearchModes.Smart
: SearchModes.Precise
}
onSelect={onSelectSearchMode}
interactive={props.interactive}
/>
) : (
<SmartSearchToggle
className="test-smart-search-toggle"
isActive={searchMode === SearchMode.SmartSearch}
onSelect={onSelectSmartSearch}
interactive={props.interactive}
/>
))}
</>
</div>
)

View File

@ -1,3 +1,4 @@
export * from './QueryInputToggle'
export * from './SmartSearchToggle'
export * from './LegacyToggles'
export * from './Toggles'

View File

@ -30,7 +30,6 @@ export const FEATURE_FLAGS = [
'enable-sveltekit',
'enable-sveltekit-toggle',
'search-content-based-lang-detection',
'search-keyword',
'search-debug',
'search-simple',
'cody-chat-mock-test',

View File

@ -8,8 +8,10 @@ import MagnifyIcon from 'mdi-react/MagnifyIcon'
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
import shallow from 'zustand/shallow'
import { Toggles } from '@sourcegraph/branded'
import { LegacyToggles } from '@sourcegraph/branded'
import { Toggles } from '@sourcegraph/branded/src/search-ui/input/toggles/Toggles'
import { SearchQueryState, SubmitSearchParameters } from '@sourcegraph/shared/src/search'
import { useExperimentalFeatures } from '@sourcegraph/shared/src/settings/settings'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
import { Text, Icon, Button, Modal, Link, ProductStatusBadge, ButtonLink } from '@sourcegraph/wildcard'
@ -182,11 +184,7 @@ const NavigationSearchBox: FC<NavigationSearchBoxProps> = props => {
const navigate = useNavigate()
const location = useLocation()
// If the feature-flag "search-keyword" is set, we allow the user to
// choose between precise (legacy), precise (new), and smart search. This
// is only temporary for internal testing. The goal is to make the new
// precise search the default.
const [showExtendedPicker] = useFeatureFlag('search-keyword')
const showKeywordSearchToggle = useExperimentalFeatures(features => features.keywordSearch)
const [isFocused, setFocused] = useState(false)
const { searchMode, queryState, searchPatternType, searchCaseSensitivity, setQueryState, submitSearch } =
@ -234,18 +232,31 @@ const NavigationSearchBox: FC<NavigationSearchBoxProps> = props => {
onChange={setQueryState}
onSubmit={submitSearchOnChange}
>
<Toggles
searchMode={searchMode}
patternType={searchPatternType}
caseSensitive={searchCaseSensitivity}
navbarSearchQuery={queryState.query}
structuralSearchDisabled={structuralSearchDisabled}
setPatternType={setSearchPatternType}
setCaseSensitivity={setSearchCaseSensitivity}
setSearchMode={setSearchMode}
submitSearch={submitSearchOnChange}
showExtendedPicker={showExtendedPicker}
/>
{showKeywordSearchToggle ? (
<Toggles
searchMode={searchMode}
patternType={searchPatternType}
caseSensitive={searchCaseSensitivity}
navbarSearchQuery={queryState.query}
structuralSearchDisabled={structuralSearchDisabled}
setPatternType={setSearchPatternType}
setCaseSensitivity={setSearchCaseSensitivity}
setSearchMode={setSearchMode}
submitSearch={submitSearchOnChange}
/>
) : (
<LegacyToggles
searchMode={searchMode}
patternType={searchPatternType}
caseSensitive={searchCaseSensitivity}
navbarSearchQuery={queryState.query}
structuralSearchDisabled={structuralSearchDisabled}
setPatternType={setSearchPatternType}
setCaseSensitivity={setSearchCaseSensitivity}
setSearchMode={setSearchMode}
submitSearch={submitSearchOnChange}
/>
)}
</LazyV2SearchInput>
{isFocused && <div className={styles.overlay} />}

View File

@ -3,15 +3,15 @@ import React, { useCallback, useRef, useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import shallow from 'zustand/shallow'
import { SearchBox, Toggles } from '@sourcegraph/branded'
import { SearchBox, LegacyToggles } from '@sourcegraph/branded'
import { Toggles } from '@sourcegraph/branded/src/search-ui/input/toggles/Toggles'
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import type { SearchContextInputProps, SubmitSearchParameters } from '@sourcegraph/shared/src/search'
import type { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
import { useExperimentalFeatures, type SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { Form } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
import { useNavbarQueryState, setSearchCaseSensitivity } from '../../stores'
import { type NavbarQueryState, setSearchMode, setSearchPatternType } from '../../stores/navbarSearchQueryState'
import { useV2QueryInput } from '../useV2QueryInput'
@ -78,11 +78,7 @@ export const SearchNavbarItem: React.FunctionComponent<React.PropsWithChildren<P
submitSearchOnChangeRef.current()
}, [])
// If the feature-flag "search-keyword" is set, we allow the user to
// choose between precise (legacy), precise (new), and smart search. This
// is only temporary for internal testing. The goal is to make the new
// precise search the default.
const [showExtendedPicker] = useFeatureFlag('search-keyword')
const showKeywordSearchToggle = useExperimentalFeatures(features => features.keywordSearch)
// TODO (#48103): Remove/simplify when new search input is released
if (v2QueryInput) {
@ -105,18 +101,35 @@ export const SearchNavbarItem: React.FunctionComponent<React.PropsWithChildren<P
selectedSearchContextSpec={props.selectedSearchContextSpec}
className="flex-grow-1"
>
<Toggles
patternType={searchPatternType}
caseSensitive={searchCaseSensitivity}
setPatternType={setSearchPatternType}
setCaseSensitivity={setSearchCaseSensitivity}
searchMode={searchMode}
setSearchMode={setSearchMode}
navbarSearchQuery={queryState.query}
submitSearch={submitSearchOnChange}
structuralSearchDisabled={window.context?.experimentalFeatures?.structuralSearch !== 'enabled'}
showExtendedPicker={showExtendedPicker}
/>
{showKeywordSearchToggle ? (
<Toggles
patternType={searchPatternType}
caseSensitive={searchCaseSensitivity}
setPatternType={setSearchPatternType}
setCaseSensitivity={setSearchCaseSensitivity}
searchMode={searchMode}
setSearchMode={setSearchMode}
navbarSearchQuery={queryState.query}
submitSearch={submitSearchOnChange}
structuralSearchDisabled={
window.context?.experimentalFeatures?.structuralSearch !== 'enabled'
}
/>
) : (
<LegacyToggles
patternType={searchPatternType}
caseSensitive={searchCaseSensitivity}
setPatternType={setSearchPatternType}
setCaseSensitivity={setSearchCaseSensitivity}
searchMode={searchMode}
setSearchMode={setSearchMode}
navbarSearchQuery={queryState.query}
submitSearch={submitSearchOnChange}
structuralSearchDisabled={
window.context?.experimentalFeatures?.structuralSearch !== 'enabled'
}
/>
)}
</LazyV2SearchInput>
</Form>
)
@ -147,7 +160,6 @@ export const SearchNavbarItem: React.FunctionComponent<React.PropsWithChildren<P
hideHelpButton={false}
showSearchHistory={true}
recentSearches={recentSearches}
showExtendedPicker={showExtendedPicker}
/>
</Form>
)

View File

@ -22,7 +22,7 @@ import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
import { useFeatureFlagOverrides } from '../../featureFlags/useFeatureFlagOverrides'
import type { CodeInsightsProps } from '../../insights/types'
import type { OwnConfigProps } from '../../own/OwnConfigProps'
import { useDeveloperSettings, useNavbarQueryState } from '../../stores'
import { setSearchPatternType, useDeveloperSettings, useNavbarQueryState } from '../../stores'
import { submitSearch } from '../helpers'
import { useRecentSearches } from '../input/useRecentSearches'
@ -213,6 +213,26 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
})
}, [caseSensitive, location, navigate, props, submittedURLQuery])
const onTogglePatternType = useCallback(
(patternType: SearchPatternType) => {
const newPatternType =
patternType !== SearchPatternType.keyword ? SearchPatternType.keyword : SearchPatternType.standard
const { selectedSearchContextSpec } = props
setSearchPatternType(newPatternType)
submitSearch({
historyOrNavigate: navigate,
location,
selectedSearchContextSpec,
caseSensitive,
patternType: newPatternType,
query: submittedURLQuery,
source: 'nav',
})
},
[caseSensitive, location, navigate, props, submittedURLQuery]
)
const hasResultsToAggregate = results?.state === 'complete' ? (results?.results.length ?? 0) > 0 : true
const showAggregationPanel = searchAggregationEnabled && hasResultsToAggregate
@ -226,6 +246,7 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
trace={!!trace}
searchContextsEnabled={props.searchContextsEnabled}
patternType={patternType}
setPatternType={setSearchPatternType}
results={results}
showAggregationPanel={showAggregationPanel}
selectedSearchContextSpec={props.selectedSearchContextSpec}
@ -243,6 +264,7 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
onExpandAllResultsToggle={onExpandAllResultsToggle}
onSearchAgain={onSearchAgain}
onDisableSmartSearch={onDisableSmartSearch}
onTogglePatternType={onTogglePatternType}
onLogSearchResultClick={logSearchResultClicked}
settingsCascade={props.settingsCascade}
telemetryService={telemetryService}

View File

@ -20,7 +20,14 @@ import { FilePrefetcher } from '@sourcegraph/shared/src/components/PrefetchableF
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { HighlightResponseFormat, SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { QueryState, QueryStateUpdate, QueryUpdate, SearchMode } from '@sourcegraph/shared/src/search'
import {
QueryState,
QueryStateUpdate,
QueryUpdate,
SearchMode,
SearchPatternTypeMutationProps,
SearchPatternTypeProps,
} from '@sourcegraph/shared/src/search'
import { stringHuman } from '@sourcegraph/shared/src/search/query/printer'
import { scanSearchQuery } from '@sourcegraph/shared/src/search/query/scanner'
import {
@ -30,7 +37,7 @@ import {
PathMatch,
StreamSearchOptions,
} from '@sourcegraph/shared/src/search/stream'
import { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
import { SettingsCascadeProps, useExperimentalFeatures } from '@sourcegraph/shared/src/settings/settings'
import { NOOP_TELEMETRY_SERVICE, TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { Button, Icon, H2, H4, useScrollManager, Panel, useLocalStorage, Link } from '@sourcegraph/wildcard'
@ -68,7 +75,9 @@ interface NewSearchContentProps
extends TelemetryProps,
SettingsCascadeProps,
PlatformContextProps,
ExtensionsControllerProps {
ExtensionsControllerProps,
SearchPatternTypeProps,
SearchPatternTypeMutationProps {
submittedURLQuery: string
queryState: QueryState
liveQuery: string
@ -76,7 +85,6 @@ interface NewSearchContentProps
searchMode: SearchMode
trace: boolean
searchContextsEnabled: boolean
patternType: SearchPatternType
results: AggregateStreamingSearchResults | undefined
showAggregationPanel: boolean
selectedSearchContextSpec: string | undefined
@ -94,6 +102,7 @@ interface NewSearchContentProps
onExpandAllResultsToggle: () => void
onSearchAgain: (additionalFilters: string[]) => void
onDisableSmartSearch: () => void
onTogglePatternType: (patternType: SearchPatternType) => void
onLogSearchResultClick: (index: number, type: string, resultsLength: number) => void
}
@ -127,6 +136,7 @@ export const NewSearchContent: FC<NewSearchContentProps> = props => {
onExpandAllResultsToggle,
onSearchAgain,
onDisableSmartSearch,
onTogglePatternType,
onLogSearchResultClick,
} = props
@ -168,6 +178,8 @@ export const NewSearchContent: FC<NewSearchContentProps> = props => {
[onSearchSubmit]
)
const showKeywordSearchToggle = useExperimentalFeatures(features => features.keywordSearch)
return (
<div className={classNames(styles.root, { [styles.rootWithNewFilters]: newFiltersEnabled })}>
{newFiltersEnabled && (
@ -216,6 +228,8 @@ export const NewSearchContent: FC<NewSearchContentProps> = props => {
className={styles.infobar}
onExpandAllResultsToggle={onExpandAllResultsToggle}
onShowMobileFiltersChanged={setSidebarCollapsed}
showKeywordSearchToggle={!!showKeywordSearchToggle}
onTogglePatternType={onTogglePatternType}
stats={
<>
<StreamingProgress

View File

@ -24,6 +24,8 @@ const COMMON_PROPS: Omit<SearchResultsInfoBarProps, 'enableCodeMonitoring'> = {
stats: <div />,
telemetryService: NOOP_TELEMETRY_SERVICE,
patternType: SearchPatternType.standard,
showKeywordSearchToggle: false,
onTogglePatternType: noop,
caseSensitive: false,
setSidebarCollapsed: noop,
sidebarCollapsed: false,

View File

@ -4,13 +4,14 @@ import { mdiChevronDoubleDown, mdiChevronDoubleUp, mdiOpenInNew, mdiThumbDown, m
import classNames from 'classnames'
import { useLocation, useNavigate } from 'react-router-dom'
import { Toggle } from '@sourcegraph/branded/src/components/Toggle'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import type { CaseSensitivityProps, SearchPatternTypeProps } from '@sourcegraph/shared/src/search'
import { FilterKind, findFilter } from '@sourcegraph/shared/src/search/query/query'
import type { AggregateStreamingSearchResults, StreamSearchOptions } from '@sourcegraph/shared/src/search/stream'
import { useExperimentalFeatures } from '@sourcegraph/shared/src/settings/settings'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { Alert, Button, Icon, Link, Text, useSessionStorage } from '@sourcegraph/wildcard'
import { Alert, Button, Icon, Label, Link, Text, useSessionStorage } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../../../auth'
import {
@ -73,6 +74,9 @@ export interface SearchResultsInfoBarProps
isSourcegraphDotCom: boolean
patternType: SearchPatternType
sourcegraphURL: string
showKeywordSearchToggle: boolean
onTogglePatternType: (patternType: SearchPatternType) => void
}
/**
@ -227,6 +231,18 @@ export const SearchResultsInfoBar: FC<SearchResultsInfoBarProps> = props => {
<div className={styles.expander} />
{props.showKeywordSearchToggle && (
<Label className={styles.toggle}>
Search language update{' '}
<Toggle
value={props.patternType === SearchPatternType.keyword}
onToggle={() => props.onTogglePatternType(props.patternType)}
title="Enable search language update"
className="mr-2"
/>
</Label>
)}
<ul className="nav align-items-center">
<SearchActionsMenu
authenticatedUser={props.authenticatedUser}

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import { SearchMode } from '@sourcegraph/shared/src/search'
import { parseSearchURL } from '../search'
@ -122,5 +123,20 @@ describe('navbar query state', () => {
expect(useNavbarQueryState.getState()).toHaveProperty('searchPatternType', SearchPatternType.regexp)
})
it('chooses correct defaults when keyword search is enabled', () => {
setQueryStateFromURL(parseSearchURL(''))
setQueryStateFromSettings({
subjects: [],
final: {
experimentalFeatures: {
keywordSearch: true,
},
},
})
expect(useNavbarQueryState.getState()).toHaveProperty('searchMode', SearchMode.Precise)
expect(useNavbarQueryState.getState()).toHaveProperty('searchPatternType', SearchPatternType.keyword)
})
})
})

View File

@ -4,7 +4,8 @@ import { useLocation, useNavigate } from 'react-router-dom'
import type { NavbarQueryState } from 'src/stores/navbarSearchQueryState'
import shallow from 'zustand/shallow'
import { SearchBox, Toggles } from '@sourcegraph/branded'
import { SearchBox, LegacyToggles } from '@sourcegraph/branded'
import { Toggles } from '@sourcegraph/branded/src/search-ui/input/toggles/Toggles'
import { TraceSpanProvider } from '@sourcegraph/observability-client'
import {
type CaseSensitivityProps,
@ -15,6 +16,7 @@ import {
type SearchModeProps,
getUserSearchContextNamespaces,
} from '@sourcegraph/shared/src/search'
import { useExperimentalFeatures } from '@sourcegraph/shared/src/settings/settings'
import { Form } from '@sourcegraph/wildcard'
import { Notices } from '../../../global/Notices'
@ -126,6 +128,8 @@ export const SearchPageInput: FC<SearchPageInputProps> = props => {
[setQueryState]
)
const showKeywordSearchToggle = useExperimentalFeatures(features => features.keywordSearch)
// TODO (#48103): Remove/simplify when new search input is released
const input = v2QueryInput ? (
<LazyV2SearchInput
@ -141,18 +145,32 @@ export const SearchPageInput: FC<SearchPageInputProps> = props => {
selectedSearchContextSpec={selectedSearchContextSpec}
className="flex-grow-1"
>
<Toggles
patternType={patternType}
caseSensitive={caseSensitive}
setPatternType={setSearchPatternType}
setCaseSensitivity={setSearchCaseSensitivity}
searchMode={searchMode}
setSearchMode={setSearchMode}
navbarSearchQuery={queryState.query}
showSmartSearchButton={false}
showExtendedPicker={false}
structuralSearchDisabled={window.context?.experimentalFeatures?.structuralSearch !== 'enabled'}
/>
{showKeywordSearchToggle ? (
<Toggles
patternType={patternType}
caseSensitive={caseSensitive}
setPatternType={setSearchPatternType}
setCaseSensitivity={setSearchCaseSensitivity}
searchMode={searchMode}
setSearchMode={setSearchMode}
navbarSearchQuery={queryState.query}
submitSearch={submitSearchOnChange}
structuralSearchDisabled={window.context?.experimentalFeatures?.structuralSearch !== 'enabled'}
/>
) : (
<LegacyToggles
patternType={patternType}
caseSensitive={caseSensitive}
setPatternType={setSearchPatternType}
setCaseSensitivity={setSearchCaseSensitivity}
searchMode={searchMode}
setSearchMode={setSearchMode}
navbarSearchQuery={queryState.query}
submitSearch={submitSearchOnChange}
showSmartSearchButton={false}
structuralSearchDisabled={window.context?.experimentalFeatures?.structuralSearch !== 'enabled'}
/>
)}
</LazyV2SearchInput>
) : (
<SearchBox

View File

@ -1,7 +1,8 @@
import { startCase } from 'lodash'
import { isErrorLike } from '@sourcegraph/common'
import type { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import { SettingsExperimentalFeatures } from '@sourcegraph/shared/src/schema/settings.schema'
import { SearchMode } from '@sourcegraph/shared/src/search'
import type { SettingsCascadeOrError, SettingsSubjectCommonFields } from '@sourcegraph/shared/src/settings/settings'
@ -34,6 +35,12 @@ export function viewerSubjectFromSettings(
* configured by the user.
*/
export function defaultSearchModeFromSettings(settingsCascade: SettingsCascadeOrError): SearchMode | undefined {
// When the 'keyword search' language update is enabled, make sure to disable smart search
const features = getFromSettings(settingsCascade, 'experimentalFeatures') as SettingsExperimentalFeatures
if (features?.keywordSearch) {
return SearchMode.Precise
}
switch (getFromSettings(settingsCascade, 'search.defaultMode')) {
case 'precise': {
return SearchMode.Precise
@ -50,6 +57,12 @@ export function defaultSearchModeFromSettings(settingsCascade: SettingsCascadeOr
* configured by the user.
*/
export function defaultPatternTypeFromSettings(settingsCascade: SettingsCascadeOrError): SearchPatternType | undefined {
// When the 'keyword search' language update is enabled, default to the 'keyword' patterntype
const features = getFromSettings(settingsCascade, 'experimentalFeatures') as SettingsExperimentalFeatures
if (features?.keywordSearch) {
return SearchPatternType.keyword
}
return getFromSettings(settingsCascade, 'search.defaultPatternType')
}

View File

@ -2513,6 +2513,8 @@ type SettingsExperimentalFeatures struct {
FuzzyFinderSymbols *bool `json:"fuzzyFinderSymbols,omitempty"`
// GoCodeCheckerTemplates description: Shows a panel with code insights templates for go code checker results.
GoCodeCheckerTemplates *bool `json:"goCodeCheckerTemplates,omitempty"`
// KeywordSearch description: Whether to enable the 'keyword search' language improvement
KeywordSearch bool `json:"keywordSearch,omitempty"`
// NewSearchNavigationUI description: Enables new experimental search UI navigation
NewSearchNavigationUI *bool `json:"newSearchNavigationUI,omitempty"`
// NewSearchResultFiltersPanel description: Enables new experimental search results filters panel
@ -2583,6 +2585,7 @@ func (v *SettingsExperimentalFeatures) UnmarshalJSON(data []byte) error {
delete(m, "fuzzyFinderRepositories")
delete(m, "fuzzyFinderSymbols")
delete(m, "goCodeCheckerTemplates")
delete(m, "keywordSearch")
delete(m, "newSearchNavigationUI")
delete(m, "newSearchResultFiltersPanel")
delete(m, "newSearchResultsUI")

View File

@ -222,6 +222,11 @@
"!go": {
"pointer": true
}
},
"keywordSearch": {
"description": "Whether to enable the 'keyword search' language improvement",
"type": "boolean",
"default": false
}
},
"group": "Experimental"