Просмотр исходного кода

Only select content when opening a file if it matches the expected text (#23324)

GitOrigin-RevId: 8b180ab897bb9027743c37f2b7faf690b7177eb6
Alf Eaton 1 год назад
Родитель
Сommit
c629355472

+ 1 - 0
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.

+ 8 - 0
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"}

+ 1 - 1
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
 }

+ 17 - 5
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)

+ 1 - 1
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()

+ 1 - 1
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

+ 161 - 0
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('<FullProjectSearch/>', 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(
+      <EditorProviders providers={{ LayoutProvider }}>
+        <FullProjectSearch />
+      </EditorProviders>
+    )
+
+    cy.findByRole('button', { name: 'Search' })
+  })
+
+  it('displays a close button', function () {
+    cy.mount(
+      <EditorProviders providers={{ LayoutProvider }}>
+        <FullProjectSearch />
+      </EditorProviders>
+    )
+
+    cy.findByRole('button', { name: 'Close' })
+  })
+
+  it('displays matched content', function () {
+    cy.mount(
+      <EditorProviders providers={{ LayoutProvider }}>
+        <FullProjectSearch />
+      </EditorProviders>
+    )
+
+    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 (
+    <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>
+  )
+}
+
+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,
+}