From e93593be09ac30d19d87b7c5fca977129b75ac46 Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Fri, 12 Sep 2025 10:20:57 +0100 Subject: [PATCH] Add locateFile to project snapshot (#28442) GitOrigin-RevId: 47c03548770b540845b0af3ba7a6c0ca89350d12 --- .../components/word-count-client.tsx | 8 ++- .../utils/count-words-in-file.ts | 28 ++++++---- .../js/infrastructure/project-snapshot.ts | 41 +++++++++++++++ .../utils/count-words-in-file.test.ts | 51 +++++++++++++------ .../word-count-modal/utils/extra-words.tex | 1 + .../word-count-modal/utils/word-count.tex | 12 +++++ 6 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 services/web/test/frontend/features/word-count-modal/utils/extra-words.tex diff --git a/services/web/frontend/js/features/word-count-modal/components/word-count-client.tsx b/services/web/frontend/js/features/word-count-modal/components/word-count-client.tsx index f5208166c4..a192c06a1e 100644 --- a/services/web/frontend/js/features/word-count-modal/components/word-count-client.tsx +++ b/services/web/frontend/js/features/word-count-modal/components/word-count-client.tsx @@ -72,7 +72,13 @@ export const WordCountClient: FC = () => { messages: '', } - countWordsInFile(data, projectSnapshot, currentRootDocPath, segmenters) + countWordsInFile( + data, + projectSnapshot, + currentRootDocPath, + '/', + segmenters + ) return data } diff --git a/services/web/frontend/js/features/word-count-modal/utils/count-words-in-file.ts b/services/web/frontend/js/features/word-count-modal/utils/count-words-in-file.ts index 1a1127f949..0d612feb17 100644 --- a/services/web/frontend/js/features/word-count-modal/utils/count-words-in-file.ts +++ b/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 { findPreambleExtent } from '@/features/word-count-modal/utils/find-preamble-extent' import { Segmenters } from './segmenters' +import { ProjectSnapshot } from '@/infrastructure/project-snapshot' // const whiteSpaceRe = /^\s$/ @@ -108,14 +109,24 @@ type TextNode = { export const countWordsInFile = ( data: WordCountData, - projectSnapshot: { getDocContents(path: string): string | null }, - docPath: string, + projectSnapshot: ProjectSnapshot, + relativePath: string, + basePath: string, 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) // TODO: try with extensions - if (!content) return + const content = projectSnapshot.getDocContents(docPath) + if (!content) { + debugConsole.warn(`No doc content in ${docPath}`) + return + } + + debugConsole.log(`Counting words in ${docPath}`) // TODO: language from file extension const tree = LaTeXLanguage.parser.parse(content) @@ -261,13 +272,10 @@ export const countWordsInFile = ( return false }, '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) if (path) { - countWordsInFile(data, projectSnapshot, path, segmenters) + countWordsInFile(data, projectSnapshot, path, docPath, segmenters) } }, 'BlankLine LineBreak'(nodeRef) { diff --git a/services/web/frontend/js/infrastructure/project-snapshot.ts b/services/web/frontend/js/infrastructure/project-snapshot.ts index 37f583d5da..e20167e902 100644 --- a/services/web/frontend/js/infrastructure/project-snapshot.ts +++ b/services/web/frontend/js/infrastructure/project-snapshot.ts @@ -85,6 +85,47 @@ export class ProjectSnapshot { 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. */ diff --git a/services/web/test/frontend/features/word-count-modal/utils/count-words-in-file.test.ts b/services/web/test/frontend/features/word-count-modal/utils/count-words-in-file.test.ts index c4ea396023..2036553a39 100644 --- a/services/web/test/frontend/features/word-count-modal/utils/count-words-in-file.test.ts +++ b/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 { createSegmenters } from '@/features/word-count-modal/utils/segmenters' import { expect } from 'chai' +import { ProjectSnapshot } from '@/infrastructure/project-snapshot' +import { Snapshot } from 'overleaf-editor-core' describe('word-count', function () { beforeEach(async function () { @@ -30,23 +32,38 @@ describe('word-count', function () { otherCharacters: 0, } 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') }) @@ -55,6 +72,7 @@ describe('word-count', function () { this.data, this.projectSnapshot, 'word-count.tex', + '/', this.segmenters ) @@ -69,8 +87,8 @@ describe('word-count', function () { headWords: 53, otherCharacters: 10, otherWords: 2, - textCharacters: 201, - textWords: 44, + textCharacters: 249, + textWords: 56, }) }) @@ -79,6 +97,7 @@ describe('word-count', function () { this.data, this.projectSnapshot, 'word-count-with-ignored-sections.tex', + '/', this.segmenters ) diff --git a/services/web/test/frontend/features/word-count-modal/utils/extra-words.tex b/services/web/test/frontend/features/word-count-modal/utils/extra-words.tex new file mode 100644 index 0000000000..b33c560673 --- /dev/null +++ b/services/web/test/frontend/features/word-count-modal/utils/extra-words.tex @@ -0,0 +1 @@ +test test diff --git a/services/web/test/frontend/features/word-count-modal/utils/word-count.tex b/services/web/test/frontend/features/word-count-modal/utils/word-count.tex index f43572a925..72d9c390ad 100644 --- a/services/web/test/frontend/features/word-count-modal/utils/word-count.tex +++ b/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} \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}