Add locateFile to project snapshot (#28442)

GitOrigin-RevId: 47c03548770b540845b0af3ba7a6c0ca89350d12
This commit is contained in:
Alf Eaton
2025-09-15 08:04:57 +00:00
committed by Copybot
parent 386e133766
commit e93593be09
6 changed files with 114 additions and 27 deletions
@@ -72,7 +72,13 @@ export const WordCountClient: FC = () => {
messages: '',
}
countWordsInFile(data, projectSnapshot, currentRootDocPath, segmenters)
countWordsInFile(
data,
projectSnapshot,
currentRootDocPath,
'/',
segmenters
)
return data
}
@@ -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) {
@@ -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.
*/
@@ -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
)
@@ -0,0 +1 @@
test test
@@ -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}