diff --git a/services/web/cypress/fixtures/blobs/5199b66d9d1226551be436c66bad9d962cc05537 b/services/web/cypress/fixtures/blobs/5199b66d9d1226551be436c66bad9d962cc05537 new file mode 100644 index 0000000000..3b1715ea8c --- /dev/null +++ b/services/web/cypress/fixtures/blobs/5199b66d9d1226551be436c66bad9d962cc05537 @@ -0,0 +1 @@ +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. diff --git a/services/web/cypress/fixtures/blobs/a0e21c740cf81e868f158e30e88985b5ea1d6c19 b/services/web/cypress/fixtures/blobs/a0e21c740cf81e868f158e30e88985b5ea1d6c19 new file mode 100644 index 0000000000..ca81447009 --- /dev/null +++ b/services/web/cypress/fixtures/blobs/a0e21c740cf81e868f158e30e88985b5ea1d6c19 @@ -0,0 +1,8 @@ +@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"} diff --git a/services/web/frontend/js/features/ide-react/types/goto-line-options.ts b/services/web/frontend/js/features/ide-react/types/goto-line-options.ts index bf4048ffab..62074b0036 100644 --- a/services/web/frontend/js/features/ide-react/types/goto-line-options.ts +++ b/services/web/frontend/js/features/ide-react/types/goto-line-options.ts @@ -1,6 +1,6 @@ export interface GotoLineOptions { gotoLine: number gotoColumn?: number - selectionLength?: number + selectText?: string syncToPdf?: boolean } diff --git a/services/web/frontend/js/features/source-editor/extensions/cursor-position.ts b/services/web/frontend/js/features/source-editor/extensions/cursor-position.ts index 3a9ccf214c..efde64f40e 100644 --- a/services/web/frontend/js/features/source-editor/extensions/cursor-position.ts +++ b/services/web/frontend/js/features/source-editor/extensions/cursor-position.ts @@ -131,20 +131,32 @@ const dispatchSelectionAndScroll = ( }) } +const selectTextIfExists = (doc: Text, pos: number, selectText: string) => { + const selectionLength = pos + selectText.length + const text = doc.sliceString(pos, selectionLength) + return text === selectText + ? EditorSelection.range(pos, selectionLength) + : EditorSelection.cursor(doc.lineAt(pos).from) +} + export const setCursorLineAndScroll = ( view: EditorView, lineNumber: number, columnNumber = 0, - selectionLength?: number + selectText?: string ) => { // TODO: map the position through any changes since the previous compile? let selectionRange try { - const pos = findValidPosition(view.state.doc, lineNumber, columnNumber) - selectionRange = selectionLength - ? EditorSelection.range(pos, pos + selectionLength) - : EditorSelection.cursor(pos) + const { doc } = view.state + const pos = findValidPosition(doc, lineNumber, columnNumber) + dispatchSelectionAndScroll( + view, + selectText + ? selectTextIfExists(doc, pos, selectText) + : EditorSelection.cursor(pos) + ) } catch (error) { // ignore invalid cursor position debugConsole.debug('invalid cursor position', error) diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts index a7de085755..796c4652a1 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts +++ b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts @@ -476,7 +476,7 @@ function useCodeMirrorScope(view: EditorView) { view, options.gotoLine, options.gotoColumn, - options.selectionLength + options.selectText ) if (options.syncToPdf) { emitSyncToPdf() diff --git a/services/web/frontend/js/shared/context/layout-context.tsx b/services/web/frontend/js/shared/context/layout-context.tsx index 2ed0cef173..8be96e291b 100644 --- a/services/web/frontend/js/shared/context/layout-context.tsx +++ b/services/web/frontend/js/shared/context/layout-context.tsx @@ -24,7 +24,7 @@ import { isMac } from '@/shared/utils/os' export type IdeLayout = 'sideBySide' | 'flat' export type IdeView = 'editor' | 'file' | 'pdf' | 'history' -type LayoutContextValue = { +export type LayoutContextValue = { reattach: () => void detach: () => void detachIsLinked: boolean diff --git a/services/web/test/frontend/features/full-project-search/components/full-project-search.spec.tsx b/services/web/test/frontend/features/full-project-search/components/full-project-search.spec.tsx new file mode 100644 index 0000000000..3fa704a4f3 --- /dev/null +++ b/services/web/test/frontend/features/full-project-search/components/full-project-search.spec.tsx @@ -0,0 +1,161 @@ +import '../../../helpers/bootstrap-5' +import { EditorProviders } from '../../../helpers/editor-providers' +import FullProjectSearch from '../../../../../modules/full-project-search/frontend/js/components/full-project-search' +import { + LayoutContext, + LayoutContextValue, +} from '@/shared/context/layout-context' +import { FC, useState } from 'react' + +describe('', function () { + beforeEach(function () { + cy.interceptCompile() + + cy.intercept('/project/*/flush', { + statusCode: 204, + }).as('project-history-flush') + + cy.intercept('/project/*/changes?*', { + body: [], + }).as('project-history-changes') + + cy.intercept('/project/*/latest/history', { + body: { chunk: mockHistoryChunk }, + }).as('project-history-snapshot') + + cy.intercept('get', '/project/*/blob/*', req => { + const blobId = req.url.split('/').pop() as string + + req.reply({ + fixture: `blobs/${blobId}`, + }) + }).as('project-history-blob') + }) + + it('displays the search form', function () { + cy.mount( + + + + ) + + cy.findByRole('button', { name: 'Search' }) + }) + + it('displays a close button', function () { + cy.mount( + + + + ) + + cy.findByRole('button', { name: 'Close' }) + }) + + it('displays matched content', function () { + cy.mount( + + + + ) + + cy.findByRole('searchbox', { name: 'Search' }).type('and{enter}') + + cy.findByRole('button', { name: 'main.tex 5' }) // TODO: remove count from name? + + cy.get('.matched-file-hit').as('matches') + cy.get('@matches').should('have.length', 5) + + cy.get('@matches').first().click() + cy.get('@matches').first().should('have.class', 'matched-file-hit-selected') + }) +}) + +const createInitialValue = () => + ({ + reattach: cy.stub(), + detach: cy.stub(), + detachIsLinked: false, + detachRole: null, + changeLayout: cy.stub(), + view: 'editor', + setView: cy.stub(), + chatIsOpen: false, + setChatIsOpen: cy.stub(), + reviewPanelOpen: false, + setReviewPanelOpen: cy.stub(), + miniReviewPanelVisible: false, + setMiniReviewPanelVisible: cy.stub(), + leftMenuShown: false, + setLeftMenuShown: cy.stub(), + loadingStyleSheet: false, + setLoadingStyleSheet: cy.stub(), + pdfLayout: 'flat', + pdfPreviewOpen: false, + projectSearchIsOpen: true, + setProjectSearchIsOpen: cy.stub(), + }) satisfies LayoutContextValue + +const LayoutProvider: FC = ({ children }) => { + const [value] = useState(createInitialValue) + + return ( + {children} + ) +} + +const mockHistoryChunk = { + 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, +}