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

Add locateFile to project snapshot (#28442)

GitOrigin-RevId: 47c03548770b540845b0af3ba7a6c0ca89350d12
Alf Eaton 11 месяцев назад
Родитель
Сommit
e93593be09

+ 7 - 1
services/web/frontend/js/features/word-count-modal/components/word-count-client.tsx

@@ -72,7 +72,13 @@ export const WordCountClient: FC = () => {
           messages: '',
           messages: '',
         }
         }
 
 
-        countWordsInFile(data, projectSnapshot, currentRootDocPath, segmenters)
+        countWordsInFile(
+          data,
+          projectSnapshot,
+          currentRootDocPath,
+          '/',
+          segmenters
+        )
 
 
         return data
         return data
       }
       }

+ 18 - 10
services/web/frontend/js/features/word-count-modal/utils/count-words-in-file.ts

@@ -4,6 +4,7 @@ import { NodeType, SyntaxNodeRef } from '@lezer/common'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 import { findPreambleExtent } from '@/features/word-count-modal/utils/find-preamble-extent'
 import { findPreambleExtent } from '@/features/word-count-modal/utils/find-preamble-extent'
 import { Segmenters } from './segmenters'
 import { Segmenters } from './segmenters'
+import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
 
 
 // const whiteSpaceRe = /^\s$/
 // const whiteSpaceRe = /^\s$/
 
 
@@ -108,14 +109,24 @@ type TextNode = {
 
 
 export const countWordsInFile = (
 export const countWordsInFile = (
   data: WordCountData,
   data: WordCountData,
-  projectSnapshot: { getDocContents(path: string): string | null },
-  docPath: string,
+  projectSnapshot: ProjectSnapshot,
+  relativePath: string,
+  basePath: string,
   segmenters: Segmenters
   segmenters: Segmenters
 ) => {
 ) => {
-  debugConsole.log(`Counting words in ${docPath}`)
+  const docPath = projectSnapshot.locateFile(relativePath, basePath)
+  if (!docPath) {
+    debugConsole.warn(`Couldn't find ${relativePath} from ${basePath}`)
+    return
+  }
+
+  const content = projectSnapshot.getDocContents(docPath)
+  if (!content) {
+    debugConsole.warn(`No doc content in ${docPath}`)
+    return
+  }
 
 
-  const content = projectSnapshot.getDocContents(docPath) // TODO: try with extensions
-  if (!content) return
+  debugConsole.log(`Counting words in ${docPath}`)
 
 
   // TODO: language from file extension
   // TODO: language from file extension
   const tree = LaTeXLanguage.parser.parse(content)
   const tree = LaTeXLanguage.parser.parse(content)
@@ -261,13 +272,10 @@ export const countWordsInFile = (
       return false
       return false
     },
     },
     'IncludeArgument InputArgument'(nodeRef) {
     'IncludeArgument InputArgument'(nodeRef) {
-      let path = content.substring(nodeRef.from + 1, nodeRef.to - 1)
-      if (!/\.\w+$/.test(path)) {
-        path += '.tex'
-      }
+      const path = content.substring(nodeRef.from + 1, nodeRef.to - 1)
       debugConsole.log(path)
       debugConsole.log(path)
       if (path) {
       if (path) {
-        countWordsInFile(data, projectSnapshot, path, segmenters)
+        countWordsInFile(data, projectSnapshot, path, docPath, segmenters)
       }
       }
     },
     },
     'BlankLine LineBreak'(nodeRef) {
     'BlankLine LineBreak'(nodeRef) {

+ 41 - 0
services/web/frontend/js/infrastructure/project-snapshot.ts

@@ -85,6 +85,47 @@ export class ProjectSnapshot {
     return paths
     return paths
   }
   }
 
 
+  /**
+   * Use an algorithm similar to Kpathsea to locate files in the project snapshot:
+   *
+   * 1. look for the exact path relative to the root path
+   * 2. look for the path + extension relative to the root path
+   * 3. look for the exact path relative to the current path
+   * 4. look for the path + extension relative to the current path
+   */
+  locateFile(filePath: string, currentPath = '/', extensions = ['.tex']) {
+    // ignore absolute paths
+    if (filePath.startsWith('/')) {
+      return null
+    }
+
+    const snapshotPaths = new Set(this.snapshot.getFilePathnames())
+
+    const baseURLs = [
+      // relative to the root of the compile directory
+      new URL('https://overleaf.invalid'),
+    ]
+
+    if (currentPath !== '/') {
+      // relative to the current directory
+      baseURLs.push(new URL(currentPath, 'https://overleaf.invalid'))
+    }
+
+    const extensionsToTest = ['', ...extensions]
+
+    for (const baseURL of baseURLs) {
+      for (const extension of extensionsToTest) {
+        const { pathname } = new URL(`${filePath}${extension}`, baseURL)
+        const snapshotPath = pathname.substring(1) // remove leading slash
+        if (snapshotPaths.has(snapshotPath)) {
+          return snapshotPath
+        }
+      }
+    }
+
+    return null
+  }
+
   /**
   /**
    * Get the doc content at the given path.
    * Get the doc content at the given path.
    */
    */

+ 35 - 16
services/web/test/frontend/features/word-count-modal/utils/count-words-in-file.test.ts

@@ -4,6 +4,8 @@ import { countWordsInFile } from '@/features/word-count-modal/utils/count-words-
 import { WordCountData } from '@/features/word-count-modal/components/word-count-data'
 import { WordCountData } from '@/features/word-count-modal/components/word-count-data'
 import { createSegmenters } from '@/features/word-count-modal/utils/segmenters'
 import { createSegmenters } from '@/features/word-count-modal/utils/segmenters'
 import { expect } from 'chai'
 import { expect } from 'chai'
+import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
+import { Snapshot } from 'overleaf-editor-core'
 
 
 describe('word-count', function () {
 describe('word-count', function () {
   beforeEach(async function () {
   beforeEach(async function () {
@@ -30,23 +32,38 @@ describe('word-count', function () {
       otherCharacters: 0,
       otherCharacters: 0,
     } satisfies WordCountData
     } satisfies WordCountData
 
 
-    const content = {
-      'word-count.tex': await readFile(
-        path.join(__dirname, 'word-count.tex'),
-        'utf-8'
-      ),
-      'word-count-with-ignored-sections.tex': await readFile(
-        path.join(__dirname, 'word-count-with-ignored-sections.tex'),
-        'utf-8'
-      ),
-    }
-
-    this.projectSnapshot = {
-      getDocContents(path: keyof typeof content) {
-        return content[path]
+    const files = {
+      'word-count.tex': {
+        content: await readFile(
+          path.join(__dirname, 'word-count.tex'),
+          'utf-8'
+        ),
+      },
+      'word-count-with-ignored-sections.tex': {
+        content: await readFile(
+          path.join(__dirname, 'word-count-with-ignored-sections.tex'),
+          'utf-8'
+        ),
+      },
+      'extra-words.tex': {
+        content: await readFile(
+          path.join(__dirname, 'extra-words.tex'),
+          'utf-8'
+        ),
+      },
+      'subfolder/extra-words.tex': {
+        content: await readFile(
+          path.join(__dirname, 'extra-words.tex'),
+          'utf-8'
+        ),
       },
       },
     }
     }
 
 
+    const projectSnapshot = new ProjectSnapshot('test')
+    // @ts-expect-error ignoring that "snapshot" is private
+    projectSnapshot.snapshot = Snapshot.fromRaw({ files })
+    this.projectSnapshot = projectSnapshot
+
     this.segmenters = createSegmenters('en_US')
     this.segmenters = createSegmenters('en_US')
   })
   })
 
 
@@ -55,6 +72,7 @@ describe('word-count', function () {
       this.data,
       this.data,
       this.projectSnapshot,
       this.projectSnapshot,
       'word-count.tex',
       'word-count.tex',
+      '/',
       this.segmenters
       this.segmenters
     )
     )
 
 
@@ -69,8 +87,8 @@ describe('word-count', function () {
       headWords: 53,
       headWords: 53,
       otherCharacters: 10,
       otherCharacters: 10,
       otherWords: 2,
       otherWords: 2,
-      textCharacters: 201,
-      textWords: 44,
+      textCharacters: 249,
+      textWords: 56,
     })
     })
   })
   })
 
 
@@ -79,6 +97,7 @@ describe('word-count', function () {
       this.data,
       this.data,
       this.projectSnapshot,
       this.projectSnapshot,
       'word-count-with-ignored-sections.tex',
       'word-count-with-ignored-sections.tex',
+      '/',
       this.segmenters
       this.segmenters
     )
     )
 
 

+ 1 - 0
services/web/test/frontend/features/word-count-modal/utils/extra-words.tex

@@ -0,0 +1 @@
+test test

+ 12 - 0
services/web/test/frontend/features/word-count-modal/utils/word-count.tex

@@ -118,4 +118,16 @@ $ 2+3 \text{ is equal to } 5 $
 \section{citations}
 \section{citations}
 \cite[two words]{example2025} % 2 in text (citekey ignored)
 \cite[two words]{example2025} % 2 in text (citekey ignored)
 
 
+% no extension
+\input{extra-words}
+\input{subfolder/extra-words}
+
+% .tex extension
+\input{extra-words.tex}
+\input{subfolder/extra-words.tex}
+
+% .tex extension, in subfolder
+\include{extra-words.tex}
+\include{subfolder/extra-words.tex}
+
 \end{document}
 \end{document}