[terraform] clsi: add pre-emp C2D capacity in zone b (#26755)

GitOrigin-RevId: aa52dec1f7135f53f8c887199c1d1e4e31ef70ff
This commit is contained in:
Jakob Ackermann
2025-07-02 10:41:44 +02:00
committed by Copybot
parent ebb2cff2af
commit 67342e9c33
13 changed files with 1337 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import React, { FC } from 'react'
import LoadingSpinner from '@/shared/components/loading-spinner'
import { MatchedFile } from '../util/search-snapshot'
import { useTranslation } from 'react-i18next'
export const FullProjectMatchCounts: FC<{
loading: boolean
totalResults?: number
matchedFiles?: MatchedFile[]
}> = ({ loading, matchedFiles }) => {
const { t } = useTranslation()
if (loading) {
return <LoadingSpinner delay={500} />
}
if (matchedFiles === undefined) {
return null
}
const totalResults = matchedFiles.flatMap(file => file.hits).length
if (totalResults === 0) {
return <>{t('project_search_result_count', { count: totalResults })}</>
}
return (
<>
{t('project_search_result_count', { count: totalResults })}{' '}
{t('project_search_file_count', { count: matchedFiles.length })}
</>
)
}

View File

@@ -0,0 +1,57 @@
import React, { FC, useMemo } from 'react'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import classnames from 'classnames'
import MaterialIcon from '@/shared/components/material-icon'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useTranslation } from 'react-i18next'
import { TooltipProps } from '@/features/ui/components/bootstrap-5/tooltip'
import { isMac } from '@/shared/utils/os'
import { sendSearchEvent } from '@/features/event-tracking/search-events'
const FullProjectSearchButton: FC = () => {
const { projectSearchIsOpen, setProjectSearchIsOpen } = useLayoutContext()
const { t } = useTranslation()
const tooltipProps: Pick<
TooltipProps,
'id' | 'description' | 'overlayProps'
> = useMemo(
() => ({
id: 'search',
description: (
<>
<div>{t('search_project')}</div>
<div>{isMac ? '⇧⌘F' : 'Ctrl+Shift+F'}</div>
</>
),
overlayProps: { placement: 'bottom' },
}),
[t]
)
return (
<OLTooltip {...tooltipProps}>
<button
className={classnames('btn', {
active: projectSearchIsOpen,
})}
onClick={() => {
if (!projectSearchIsOpen) {
sendSearchEvent('search-open', {
searchType: 'full-project',
method: 'button',
location: 'toolbar',
})
}
setProjectSearchIsOpen(value => !value)
}}
tabIndex={-1}
data-active={projectSearchIsOpen}
>
<MaterialIcon type="search" accessibilityLabel={t('search')} />
</button>
</OLTooltip>
)
}
export default FullProjectSearchButton

View File

@@ -0,0 +1,78 @@
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
import { SearchQuery } from '@codemirror/search'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import { Form } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
export const FullProjectSearchModifiers = forwardRef(
function FullProjectSearchModifiers(props, ref) {
const { t } = useTranslation()
const caseSensitiveRef = useRef<HTMLInputElement>(null)
const regexpRef = useRef<HTMLInputElement>(null)
const wholeWordRef = useRef<HTMLInputElement>(null)
useImperativeHandle(ref, () => {
return {
setQuery(query: SearchQuery) {
caseSensitiveRef.current!.checked = query.caseSensitive
regexpRef.current!.checked = query.regexp
wholeWordRef.current!.checked = query.wholeWord
},
}
}, [])
return (
<div className="full-project-search-modifiers" role="group">
<OLTooltip
id="project-search-caseSensitive"
description={t('search_match_case')}
overlayProps={{ placement: 'bottom' }}
>
<span>
<Form.Check
inline
label="Aa"
name="caseSensitive"
type="checkbox"
id="project-search-caseSensitive"
ref={caseSensitiveRef}
/>
</span>
</OLTooltip>
<OLTooltip
id="project-search-regexp"
description={t('search_regexp')}
overlayProps={{ placement: 'bottom' }}
>
<span>
<Form.Check
inline
label="[.*]"
name="regexp"
type="checkbox"
id="project-search-regexp"
ref={regexpRef}
/>
</span>
</OLTooltip>
<OLTooltip
id="project-search-wholeWord"
description={t('search_whole_word')}
overlayProps={{ placement: 'bottom' }}
>
<span>
<Form.Check
inline
label="W"
name="wholeWord"
type="checkbox"
id="project-search-wholeWord"
ref={wholeWordRef}
/>
</span>
</OLTooltip>
</div>
)
}
)

View File

@@ -0,0 +1,174 @@
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
import { Hit, MatchedFile } from '../util/search-snapshot'
import classnames from 'classnames'
import { CollapsibleFileHeader } from '@/shared/components/collapsible-file-header'
import { MatchedHit } from './matched-hit'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import { sendSearchEvent } from '@/features/event-tracking/search-events'
export const FullProjectSearchResults: FC<{
matchedFiles: MatchedFile[]
currentDocPath: string | null
}> = ({ matchedFiles, currentDocPath }) => {
const [collapsedFiles, setCollapsedFiles] = useState<Set<string>>(new Set())
const [selectedHit, setSelectedHit] = useState<Hit>()
const toggleCollapse = useCallback((path: string) => {
setCollapsedFiles(value => {
const newValue = new Set(value)
if (newValue.has(path)) {
newValue.delete(path)
} else {
newValue.add(path)
}
return newValue
})
}, [])
const resultsContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const container = resultsContainerRef.current
if (container) {
const hits = matchedFiles.flatMap(file => file.hits)
const findSelectedHitIndex = () =>
hits.findIndex(hit => hit === selectedHit)
const listener = (event: KeyboardEvent) => {
if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
return
}
if (!matchedFiles) {
return
}
switch (event.key) {
case 'Enter':
case ' ': // Space
window.setTimeout(() => {
window.dispatchEvent(new Event('editor:focus'))
})
break
case 'ArrowUp':
{
event.preventDefault()
let index = findSelectedHitIndex()
if (index === 0) {
index = hits.length
}
index--
if (index < 0) {
index = 0
}
setSelectedHit(hits[index])
}
break
case 'ArrowDown':
{
event.preventDefault()
let index = findSelectedHitIndex()
index++
if (index >= hits.length) {
index = 0
}
setSelectedHit(hits[index])
}
break
}
}
container.addEventListener('keydown', listener)
return () => {
container.removeEventListener('keydown', listener)
}
}
}, [matchedFiles, selectedHit, setSelectedHit])
const { findEntityByPath } = useFileTreePathContext()
const { openDocWithId, openFileWithId } = useEditorManagerContext()
const selectedHitRef = useRef<Hit>()
useEffect(() => {
// only open the doc if selectedHit has actually changed
if (selectedHit && selectedHit !== selectedHitRef.current) {
selectedHitRef.current = selectedHit
const selectedFile = matchedFiles.find(file =>
file.hits.includes(selectedHit)
)
if (selectedFile) {
const result = findEntityByPath(selectedFile.path)
if (result) {
sendSearchEvent('search-result-click', {
searchType: 'full-project',
})
const line = selectedHit.lineIndex
const column = selectedHit.matchIndex
const text = selectedFile.lines[line].substring(
column,
column + selectedHit.length
)
if (result.type === 'doc') {
openDocWithId(result.entity._id, {
gotoLine: line + 1,
gotoColumn: column,
selectText: text,
})
} else if (result.type === 'fileRef') {
openFileWithId(result.entity._id)
}
}
}
}
}, [
findEntityByPath,
matchedFiles,
openDocWithId,
openFileWithId,
selectedHit,
])
const tabbableHit = selectedHit ?? matchedFiles?.[0]?.hits[0]
return (
<div className="matched-files" ref={resultsContainerRef}>
{matchedFiles.map(matchedFile => (
<div
key={matchedFile.path}
className={classnames('matched-file', {
'matched-file-open': currentDocPath === matchedFile.path,
})}
>
<CollapsibleFileHeader
name={matchedFile.path}
count={matchedFile.hits.length}
collapsed={collapsedFiles.has(matchedFile.path)}
toggleCollapsed={() => toggleCollapse(matchedFile.path)}
/>
{!collapsedFiles.has(matchedFile.path) && (
<div className="list-group matched-file-hits" role="listbox">
{matchedFile.hits.map(hit => {
return (
<MatchedHit
key={`${hit.lineIndex}:${hit.matchIndex}`}
matchedFile={matchedFile}
hit={hit}
selected={hit === selectedHit}
setSelectedHit={setSelectedHit}
tabIndex={tabbableHit === hit ? 0 : -1}
/>
)
})}
</div>
)}
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,250 @@
import React, {
FC,
FormEventHandler,
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useProjectContext } from '@/shared/context/project-context'
import {
MatchedFile as MatchedFileType,
searchSnapshot,
} from '../util/search-snapshot'
import { SearchQuery } from '@codemirror/search'
import { debugConsole } from '@/utils/debugging'
import useEventListener from '@/shared/hooks/use-event-listener'
import { Col, Form, Row } from 'react-bootstrap'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
import Button from '@/features/ui/components/bootstrap-5/button'
import Notification from '@/shared/components/notification'
import '../../stylesheets/full-project-search.scss'
import { userStyles } from '@/shared/utils/styles'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { FullProjectMatchCounts } from './full-project-match-counts'
import { FullProjectSearchModifiers } from './full-project-search-modifiers'
import { isMac } from '@/shared/utils/os'
import { PanelHeading } from '@/shared/components/panel-heading'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import { createRegExp } from '../util/regexp'
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { FullProjectSearchResults } from './full-project-search-results'
import { signalWithTimeout } from '@/utils/abort-signal'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
import RailPanelHeader from '@/features/ide-redesign/components/rail-panel-header'
const FullProjectSearchUI: FC = () => {
const { t } = useTranslation()
const { setProjectSearchIsOpen } = useLayoutContext()
const { projectSnapshot } = useProjectContext()
const { openDocs } = useEditorManagerContext()
const { pathInFolder } = useFileTreePathContext()
const newEditor = useIsNewEditorEnabled()
const { currentDocument: currentDoc } = useEditorOpenDocContext()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string>()
const [matchedFiles, setMatchedFiles] = useState<MatchedFileType[]>()
const { userSettings } = useUserSettingsContext()
const { fontFamily, fontSize } = useMemo(
() => userStyles(userSettings),
[userSettings]
)
const abortControllerRef = useRef<AbortController | null>(null)
// start fetching the snapshot when the project search UI opens
useEffect(() => {
projectSnapshot.refresh().catch(error => {
debugConsole.error(error)
})
}, [projectSnapshot])
const currentDocPath = useMemo(() => {
return currentDoc && pathInFolder(currentDoc.doc_id)
}, [currentDoc, pathInFolder])
const handleSubmit: React.FormEventHandler<HTMLFormElement> = useCallback(
async event => {
event.preventDefault()
setMatchedFiles(undefined)
abortControllerRef.current?.abort()
abortControllerRef.current = new AbortController()
const data = new FormData(event.target as HTMLFormElement)
const searchQuery = new SearchQuery({
search: data.get('search') as string,
// replace: data.get('replace') as string,
caseSensitive: data.get('caseSensitive') === 'on',
regexp: data.get('regexp') === 'on',
wholeWord: data.get('wholeWord') === 'on',
literal: data.get('regexp') !== 'on',
})
if (searchQuery.regexp) {
try {
createRegExp(searchQuery)
} catch (error) {
setError(t('invalid_regular_expression'))
return
}
}
setLoading(true)
setError(undefined)
try {
await openDocs.awaitBufferedOps(
signalWithTimeout(abortControllerRef.current.signal, 5000)
)
await projectSnapshot.refresh()
if (!abortControllerRef.current.signal.aborted) {
const results = await searchSnapshot(projectSnapshot, searchQuery)
setMatchedFiles(results)
}
} catch (error) {
debugConsole.error(error)
setError(t('generic_something_went_wrong'))
} finally {
setLoading(false)
}
},
[openDocs, projectSnapshot, t]
)
const searchInputRef = useRef<HTMLInputElement>(null)
const handleKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback(
event => {
if (event.key === 'Escape') {
setProjectSearchIsOpen(false)
}
},
[setProjectSearchIsOpen]
)
useEventListener(
'keydown',
useCallback((event: KeyboardEvent) => {
if (
(isMac ? event.metaKey : event.ctrlKey) &&
event.shiftKey &&
event.code === 'KeyF'
) {
searchInputRef.current?.focus()
}
}, [])
)
const modifiersRef = useRef<{ setQuery(query: SearchQuery): void }>(null)
useEventListener(
'editor:full-project-search',
useCallback((event: CustomEvent<SearchQuery>) => {
if (modifiersRef.current != null) {
modifiersRef.current.setQuery(event.detail)
}
if (searchInputRef.current != null) {
searchInputRef.current.value = event.detail.search
searchInputRef.current.form?.dispatchEvent(
new Event('submit', { cancelable: true, bubbles: true })
)
}
}, [])
)
// clear the results when the form is cleared
const handleInput: FormEventHandler<HTMLInputElement> = useCallback(event => {
if (
event instanceof InputEvent &&
event.inputType === undefined &&
(event.target as HTMLInputElement).value.length === 0
) {
setMatchedFiles(undefined)
}
}, [])
const variableStyle = {
'--font-family': fontFamily,
'--font-size': fontSize,
} as React.CSSProperties
return (
<div
className="full-project-search"
style={variableStyle}
data-bs-theme={userSettings.overallTheme === 'light-' ? 'light' : 'dark'}
>
{newEditor ? (
<RailPanelHeader title={t('search')} />
) : (
<PanelHeading
title={t('search')}
handleClose={() => setProjectSearchIsOpen(false)}
splitTestName="full-project-search"
/>
)}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
className="full-project-search-form"
onKeyDown={handleKeyDown}
>
<Form
onSubmit={handleSubmit}
role="search"
id="full-project-search"
aria-label={t('search_all_project_files')}
>
<Row className="g-1">
<Col>
<OLFormControl
type="search"
name="search"
size="sm"
aria-label={t('search')}
autoFocus // eslint-disable-line jsx-a11y/no-autofocus
spellCheck={false}
autoComplete="off"
ref={searchInputRef}
onInput={handleInput}
placeholder={`${t('search_all_project_files')}`}
/>
</Col>
<Col className="col-auto">
<Button type="submit" className="btn btn-primary" size="sm">
{t('search')}
</Button>
</Col>
</Row>
<FullProjectSearchModifiers ref={modifiersRef} />
</Form>
</div>
{error && <Notification type="error" content={error} />}
<div className="match-counts">
<FullProjectMatchCounts loading={loading} matchedFiles={matchedFiles} />
</div>
{matchedFiles && (
<FullProjectSearchResults
matchedFiles={matchedFiles}
currentDocPath={currentDocPath}
/>
)}
</div>
)
}
export default memo(FullProjectSearchUI)

View File

@@ -0,0 +1,22 @@
import React, { FC, lazy, Suspense } from 'react'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
const FullProjectSearchUI = lazy(() => import('./full-project-search-ui'))
const FullProjectSearch: FC = () => {
const { projectSearchIsOpen } = useLayoutContext()
const newEditor = useIsNewEditorEnabled()
if (!projectSearchIsOpen && !newEditor) {
return null
}
return (
<Suspense fallback={null}>
<FullProjectSearchUI />
</Suspense>
)
}
export default FullProjectSearch

View File

@@ -0,0 +1,49 @@
import React, { FC, useMemo } from 'react'
import { Hit } from '../util/search-snapshot'
export const MatchedHitHighlight: FC<{ text: string; hit: Hit }> = ({
text,
hit,
}) => {
const parts = useMemo(() => {
let before = text.substring(0, hit.matchIndex).trimStart()
const match = text.substring(hit.matchIndex, hit.matchIndex + hit.length)
let after = text.substring(hit.matchIndex + hit.length).trimEnd()
// reduce the prefix to a sensible size before trimming
if (before.length > 250) {
before = before.substring(before.length - 250)
}
while (before.length > 10) {
const replacement = before.replace(/^\S+\s+/, '')
if (before.length === replacement.length) {
break
}
before = replacement
}
// reduce the suffix to a sensible size before trimming
if (after.length > 250) {
after = after.substring(0, 250)
}
while (after.length > 100) {
const replacement = after.replace(/\s+\S+$/, '')
if (after.length === replacement.length) {
break
}
after = replacement
}
return { before, match, after }
}, [hit, text])
return (
<span className="matched-hit-snippet">
{parts.before}
<b className="matched-hit-highlight">{parts.match}</b>
{parts.after}
</span>
)
}

View File

@@ -0,0 +1,45 @@
import { FC, useCallback, useLayoutEffect, useRef } from 'react'
import { Hit, MatchedFile } from '../util/search-snapshot'
import { MatchedHitHighlight } from './matched-hit-highlight'
import classnames from 'classnames'
export const MatchedHit: FC<{
matchedFile: MatchedFile
hit: Hit
selected?: boolean
setSelectedHit(hit?: Hit): void
tabIndex: 0 | -1
}> = ({ matchedFile, hit, selected = false, setSelectedHit, tabIndex }) => {
const containerRef = useRef<HTMLButtonElement>(null)
useLayoutEffect(() => {
if (selected) {
containerRef.current?.focus()
}
}, [selected])
const handleSelect: React.MouseEventHandler = useCallback(
event => {
event.preventDefault()
setSelectedHit(hit)
},
[hit, setSelectedHit]
)
return (
<button
className={classnames('list-group-item matched-file-hit', {
'matched-file-hit-selected': selected,
})}
ref={containerRef}
tabIndex={tabIndex}
onMouseDown={handleSelect}
aria-selected={selected}
role="option"
>
<span className="matched-line-number">{hit.lineIndex + 1}</span>
<MatchedHitHighlight text={matchedFile.lines[hit.lineIndex]} hit={hit} />
</button>
)
}

View File

@@ -0,0 +1,7 @@
import { SearchQuery } from '@codemirror/search'
export const createRegExp = (searchQuery: SearchQuery) => {
const flags = 'gmu' + (searchQuery.caseSensitive ? '' : 'i')
return new RegExp(searchQuery.search, flags)
}

View File

@@ -0,0 +1,92 @@
import { Text } from '@codemirror/state'
import { RegExpCursor, SearchCursor, SearchQuery } from '@codemirror/search'
import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
import { categorizer, regexpWordTest, stringWordTest } from './search'
import { sendSearchEvent } from '@/features/event-tracking/search-events'
export type Hit = {
lineIndex: number
matchIndex: number
length: number
}
export type MatchedFile = {
path: string
lines: string[]
hits: Hit[]
}
const toLowerCase = (string: string) => string.toLowerCase()
export const searchSnapshot = async (
projectSnapshot: ProjectSnapshot,
searchQuery: SearchQuery
) => {
if (!searchQuery.search.trim().length) {
return
}
const matchedFiles = new Map<string, MatchedFile>()
const createCursor = (text: Text) => {
if (searchQuery.regexp) {
return new RegExpCursor(text, searchQuery.search, {
ignoreCase: !searchQuery.caseSensitive,
test: searchQuery.wholeWord ? regexpWordTest(categorizer) : undefined,
})
}
return new SearchCursor(
text,
searchQuery.search,
undefined,
undefined,
searchQuery.caseSensitive ? undefined : toLowerCase,
searchQuery.wholeWord ? stringWordTest(text, categorizer) : undefined
)
}
const docPaths = projectSnapshot.getDocPaths()
for (const path of docPaths) {
const content = projectSnapshot.getDocContents(path)
if (content) {
const lines = content.split('\n')
const text = Text.of(lines)
const cursor = createCursor(text)
while (!cursor.next().done) {
const { from, to } = cursor.value
const matchedFile: MatchedFile = matchedFiles.get(path) ?? {
path,
lines,
hits: [],
}
const line = text.lineAt(from)
matchedFile.hits.push({
lineIndex: line.number - 1,
matchIndex: from - line.from,
length: to - from,
})
matchedFiles.set(path, matchedFile)
}
}
}
const results = [...matchedFiles.values()].sort((a, b) =>
a.path.localeCompare(b.path)
)
sendSearchEvent('search-execute', {
searchType: 'full-project',
totalDocs: docPaths.length,
totalResults: results.flatMap(file => file.hits).length,
})
return results
}

View File

@@ -0,0 +1,50 @@
/**
* The functions in this module are from @codemirror/search (MIT license)
* https://github.com/codemirror/search/blob/c1ee7d4b0babd0de0d1198a7c1ece2a387c97c0d/src/search.ts
*/
import { CharCategory, findClusterBreak, Text } from '@codemirror/state'
const charBefore = (str: string, index: number) =>
str.slice(findClusterBreak(str, index, false), index)
const charAfter = (str: string, index: number) =>
str.slice(index, findClusterBreak(str, index))
export const categorizer = (char: string) => {
if (/\s/.test(char)) {
return CharCategory.Space
}
if (/\w/.test(char)) {
return CharCategory.Word
}
return CharCategory.Other
}
export const stringWordTest =
(doc: Text, categorizer: (ch: string) => CharCategory) =>
(from: number, to: number, buf: string, bufPos: number) => {
if (bufPos > from || bufPos + buf.length < to) {
bufPos = Math.max(0, from - 2)
buf = doc.sliceString(bufPos, Math.min(doc.length, to + 2))
}
return (
(categorizer(charBefore(buf, from - bufPos)) !== CharCategory.Word ||
categorizer(charAfter(buf, from - bufPos)) !== CharCategory.Word) &&
(categorizer(charAfter(buf, to - bufPos)) !== CharCategory.Word ||
categorizer(charBefore(buf, to - bufPos)) !== CharCategory.Word)
)
}
export const regexpWordTest =
(categorizer: (ch: string) => CharCategory) =>
(_from: number, _to: number, match: RegExpExecArray) =>
!match[0].length ||
((categorizer(charBefore(match.input, match.index)) !== CharCategory.Word ||
categorizer(charAfter(match.input, match.index)) !== CharCategory.Word) &&
(categorizer(charAfter(match.input, match.index + match[0].length)) !==
CharCategory.Word ||
categorizer(charBefore(match.input, match.index + match[0].length)) !==
CharCategory.Word))

View File

@@ -0,0 +1,367 @@
@use 'sass:color';
.ide-redesign-main {
.full-project-search {
--full-project-search-bg-color: var(--white);
--full-project-search-color: var(--content-primary);
--full-project-search-results-bg-color: var(--bg-light-primary);
--full-project-search-selected-hit-bg-color: var(--bg-accent-03);
--full-project-search-selected-hit-color: var(--green-70);
--matched-hit-highlight-color: var(--yellow-10);
--matched-hit-selected-highlight-color: var(--bg-accent-02);
--matched-hit-selected-unfocused-highlight-color: var(--bg-accent-02);
--collapsible-file-header-count-color: var(--content-primary);
--collapsible-file-header-count-bg-color: var(--bg-light-tertiary);
// Redesign additions
--full-project-search-border-color: var(--border-divider);
--collapsible-file-header-hover-bg: var(--bg-light-secondary);
--matched-hit-highlight-text-color: var(--content-primary);
--matched-file-hit-hover-bg: var(--bg-light-secondary);
--search-modifier-checked-bg: var(--bg-accent-03);
--search-modifier-checked-color: var(--green-70);
--search-modifier-hover-color: var(--bg-light-secondary);
--search-modifier-color: var(--content-primary);
&[data-bs-theme='dark'] {
--full-project-search-bg-color: var(--bg-dark-primary);
--full-project-search-color: var(--content-secondary-dark);
--full-project-search-results-bg-color: var(--bg-dark-primary);
--full-project-search-selected-hit-bg-color: var(--green-70);
--full-project-search-selected-hit-color: var(--green-10);
--matched-hit-highlight-color: var(--yellow-70);
--matched-hit-selected-highlight-color: var(--green-70);
--matched-hit-selected-unfocused-highlight-color: var(--content-primary);
--collapsible-file-header-count-color: var(--content-primary);
--collapsible-file-header-count-bg-color: var(--bg-light-tertiary);
--panel-heading-color: var(--content-primary-dark);
// Redesign additions
--full-project-search-border-color: var(--border-divider-dark);
--collapsible-file-header-hover-bg: var(--bg-dark-secondary);
--matched-hit-highlight-text-color: var(--green-10);
--matched-file-hit-hover-bg: var(--bg-dark-secondary);
--search-modifier-checked-bg: var(--green-70);
--search-modifier-checked-color: var(--green-10);
--search-modifier-hover-color: var(--bg-dark-secondary);
--search-modifier-color: var(--content-primary-dark);
input[type='search']::selection {
background-color: #b4d1ff;
}
}
.full-project-search-modifiers {
input:checked ~ .form-check-label {
background-color: var(--search-modifier-checked-bg);
color: var(--search-modifier-checked-color);
}
.form-check-label {
color: var(--search-modifier-color);
&:hover,
&:focus {
background-color: var(--search-modifier-hover-color);
}
}
}
.full-project-search-form {
border-bottom: 1px solid var(--full-project-search-border-color);
}
.match-counts {
padding: var(--spacing-03) var(--spacing-04);
font-size: var(--font-size-01);
}
.matched-files {
padding: 0 var(--spacing-02);
}
.matched-hit-highlight {
color: var(--matched-hit-highlight-text-color);
}
.matched-file-hits {
width: unset;
overflow: unset;
margin-left: var(--spacing-08);
position: relative;
&::before {
content: '';
position: absolute;
left: calc(var(--spacing-04) * -1);
top: 0;
width: 1px;
height: 100%;
background-color: var(--full-project-search-border-color);
border-radius: 0;
}
}
.matched-file-hit {
border-radius: var(--border-radius-base);
&:hover {
background-color: var(--matched-file-hit-hover-bg);
}
&.matched-file-hit-selected:hover {
background-color: var(--full-project-search-selected-hit-bg-color);
color: var(--full-project-search-selected-hit-color);
}
}
.collapsible-file-header {
border-radius: var(--border-radius-base);
&:hover {
background-color: var(--collapsible-file-header-hover-bg);
}
}
}
}
.full-project-search {
--full-project-search-bg-color: var(--bg-light-secondary);
--full-project-search-color: var(--content-secondary);
--full-project-search-results-bg-color: var(--bg-light-primary);
--full-project-search-selected-hit-bg-color: var(--bg-light-tertiary);
--full-project-search-selected-hit-color: var(--content-primary-light);
--matched-hit-highlight-color: var(--bg-light-tertiary);
--matched-hit-selected-highlight-color: var(--bg-accent-02);
--matched-hit-selected-unfocused-highlight-color: var(--bg-accent-02);
--collapsible-file-header-count-color: var(--bs-body-color);
--collapsible-file-header-count-bg-color: var(--bg-light-secondary);
&[data-bs-theme='dark'] {
--full-project-search-bg-color: var(--bg-dark-secondary);
--full-project-search-color: var(--content-secondary-dark);
--full-project-search-results-bg-color: var(--bg-dark-tertiary);
--full-project-search-selected-hit-bg-color: var(--bg-dark-secondary);
--full-project-search-selected-hit-color: var(--content-primary-dark);
--matched-hit-highlight-color: var(--bg-dark-secondary);
--matched-hit-selected-highlight-color: var(--bg-accent-02);
--matched-hit-selected-unfocused-highlight-color: var(--bg-dark-primary);
--collapsible-file-header-count-color: var(--bs-body-color);
--collapsible-file-header-count-bg-color: var(--bg-dark-secondary);
--panel-heading-color: var(--content-primary-dark);
input[type='search']::selection {
background-color: #b4d1ff;
}
.full-project-search-modifiers {
input:checked ~ .form-check-label {
background-color: var(--neutral-90);
}
.form-check-label {
color: inherit;
&:hover,
&:focus {
background-color: var(--neutral-70);
}
}
}
.panel-heading-close-button {
color: var(--content-primary-dark);
&:hover,
&:focus {
color: var(--content-primary);
}
}
}
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 2;
background-color: var(--full-project-search-bg-color);
color: var(--full-project-search-color);
.full-project-search-heading {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-02);
font-weight: bold;
}
.panel-heading-label {
margin-left: var(--spacing-02);
}
.full-project-search-form {
padding: 8px;
flex-shrink: 0;
}
.form-check {
padding-left: 0;
}
.form-check-inline {
display: inline-flex;
gap: var(--spacing-02);
margin-right: 0;
}
.full-project-search-modifiers {
display: flex;
align-items: center;
gap: var(--spacing-03);
margin-top: var(--spacing-02);
input[type='checkbox'] {
position: relative;
left: -999px;
}
.form-check-label {
border-radius: 50%;
cursor: pointer;
display: inline-flex;
width: 2em;
height: 2em;
align-items: center;
justify-content: center;
font-size: 90%;
&:hover,
&:focus {
background-color: var(--neutral-20);
}
}
input:focus-visible ~ .form-check-label {
box-shadow: 0 0 0 2px var(--border-active-dark);
}
input:checked ~ .form-check-label {
background-color: var(--neutral-30);
}
}
.match-counts {
font-size: 80%;
padding: 0 var(--spacing-04) var(--spacing-04);
}
.matched-files {
flex: 1;
overflow: auto;
font-size: 14px;
background-color: var(--full-project-search-results-bg-color);
}
.matched-file {
margin-bottom: 8px;
.collapsible-file-header:focus-visible {
box-shadow: 0 0 0 2px var(--border-active-dark);
}
}
.collapsible-file-header {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--full-project-search-results-bg-color);
color: inherit;
}
.collapsible-file-header-count {
background-color: var(--collapsible-file-header-count-bg-color);
color: var(--collapsible-file-header-count-color);
}
.matched-file-hits {
width: 100%;
overflow: hidden;
border-radius: 0;
}
.matched-line-number {
white-space: nowrap;
margin-right: 8px;
color: #aaa;
}
.matched-file-hit {
border: none;
cursor: pointer;
align-items: flex-start;
padding: var(--spacing-02) var(--spacing-05);
word-break: break-all;
white-space: nowrap;
text-align: left;
font-family: var(--font-family);
font-size: var(--font-size);
min-height: fit-content;
border-radius: 0;
background-color: var(--full-project-search-results-bg-color);
&:hover {
background-color: var(--full-project-search-bg-color);
}
&.matched-file-hit-highlighted {
background-color: var(--full-project-search-bg-color);
}
}
.matched-hit-snippet {
overflow-wrap: break-word;
overflow: hidden;
}
.notification {
margin: var(--spacing-02) var(--spacing-04) var(--spacing-06);
width: auto;
align-items: center;
padding: 0 var(--spacing-04);
.notification-content {
padding: var(--spacing-02) 0;
}
.notification-icon {
padding: var(--spacing-02) var(--spacing-04) 0 0;
}
}
.matched-hit-highlight {
background-color: var(--matched-hit-highlight-color);
border-radius: 4px;
overflow-wrap: normal;
}
.matched-file-hit-selected {
background-color: var(--full-project-search-selected-hit-bg-color);
color: var(--full-project-search-selected-hit-color);
.matched-hit-highlight {
background-color: var(--matched-hit-unfocused-highlight-color);
}
}
/* &:focus-within {
.matched-file-hit-selected {
background-color: var(--bg-accent-01);
color: var(--bg-light-primary);
.matched-hit-highlight {
background-color: var(--matched-hit-selected-highlight-color);
}
}
} */
}

View File

@@ -0,0 +1,113 @@
import { Meta, StoryObj } from '@storybook/react'
import { ScopeDecorator } from '../../../frontend/stories/decorators/scope'
import useFetchMock from '../../../frontend/stories/hooks/use-fetch-mock'
import FullProjectSearchUI from '../frontend/js/components/full-project-search-ui'
const meta = {
title: 'Editor / Full Project Search',
component: FullProjectSearchUI,
decorators: [
Story => ScopeDecorator(Story, { mockCompileOnLoad: true }),
Story => {
useFetchMock(fetchMock => {
fetchMock.post('express:/project/:projectId/flush', { status: 204 })
fetchMock.get('express:/project/:projectId/latest/history', {
status: 200,
body: {
chunk: {
history: {
snapshot: {
files: {},
},
changes: [
{
operations: [
{
pathname: 'main.tex',
file: {
hash: '5199b66d9d1226551be436c66bad9d962cc05537',
stringLength: 7066,
},
},
],
timestamp: '2025-01-03T10:10:40.840Z',
authors: [],
v2Authors: ['66e040e0da7136ec75ffe8a3'],
projectVersion: '1.0',
},
{
operations: [
{
pathname: 'sample.bib',
file: {
hash: 'a0e21c740cf81e868f158e30e88985b5ea1d6c19',
stringLength: 244,
},
},
],
timestamp: '2025-01-03T10:10:40.856Z',
authors: [],
v2Authors: ['66e040e0da7136ec75ffe8a3'],
projectVersion: '2.0',
},
{
operations: [
{
pathname: 'frog.jpg',
file: {
hash: '5b889ef3cf71c83a4c027c4e4dc3d1a106b27809',
byteLength: 97080,
},
},
],
timestamp: '2025-01-03T10:10:40.890Z',
authors: [],
v2Authors: ['66e040e0da7136ec75ffe8a3'],
projectVersion: '3.0',
},
],
},
startVersion: 0,
},
},
})
fetchMock.get('express:/project/:projectId/changes', {
status: 200,
body: [],
})
fetchMock.get(
'express:/project/:projectId/blob/5199b66d9d1226551be436c66bad9d962cc05537',
{
status: 200,
body: `Simply use the section and subsection commands, as in this example document! With Overleaf, all the formatting and numbering is handled automatically according to the template you've chosen. If you're using the Visual Editor, you can also create new section and subsections via the buttons in the editor toolbar.`,
}
)
fetchMock.get(
'express:/project/:projectId/blob/a0e21c740cf81e868f158e30e88985b5ea1d6c19',
{
status: 200,
body: `@article{greenwade93,
author = "George D. Greenwade",
title = "The {C}omprehensive {T}ex {A}rchive {N}etwork ({CTAN})",
year = "1993",
journal = "TUGBoat",
volume = "14",
number = "3",
pages = "342--351"
}`,
}
)
})
return <Story />
},
],
} satisfies Meta<typeof FullProjectSearchUI>
export default meta
type Story = StoryObj<typeof FullProjectSearchUI>
export const UI = {} satisfies Story