Skip to content

Commit

Permalink
Support file watching on virtual file systems (#4359)
Browse files Browse the repository at this point in the history
* Support file watching on virtual file systems

* Revert some changes

* Only allow URI in watcher arguments
  • Loading branch information
msujew authored Sep 2, 2024
1 parent 2cdc61b commit 5b0f7ab
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 127 deletions.
6 changes: 3 additions & 3 deletions src/compile/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export {
build,
}

lw.watcher.src.onChange(filePath => autoBuild(filePath, 'onFileChange'))
lw.watcher.bib.onChange(filePath => autoBuild(filePath, 'onFileChange', true))
lw.watcher.src.onChange(filePath => autoBuild(filePath.fsPath, 'onFileChange'))
lw.watcher.bib.onChange(filePath => autoBuild(filePath.fsPath, 'onFileChange', true))

/**
* Triggers auto build based on file change or file save events. If the
Expand Down Expand Up @@ -420,7 +420,7 @@ async function afterSuccessfulBuilt(lastStep: Step, skipped: boolean) {
if (!lastStep.isExternal && skipped) {
return
}
lw.viewer.refresh(lw.file.getPdfPath(lastStep.rootFile))
lw.viewer.refresh(vscode.Uri.file(lw.file.getPdfPath(lastStep.rootFile)))
lw.completion.reference.setNumbersFromAuxFile(lastStep.rootFile)
await lw.cache.loadFlsFile(lastStep.rootFile ?? '')
const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(lastStep.rootFile))
Expand Down
6 changes: 3 additions & 3 deletions src/completion/completer/citation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const data = {
bibEntries: new Map<string, CitationItem[]>()
}

lw.watcher.bib.onCreate(filePath => parseBibFile(filePath))
lw.watcher.bib.onChange(filePath => parseBibFile(filePath))
lw.watcher.bib.onDelete(filePath => removeEntriesInFile(filePath))
lw.watcher.bib.onCreate(uri => parseBibFile(uri.fsPath))
lw.watcher.bib.onChange(uri => parseBibFile(uri.fsPath))
lw.watcher.bib.onDelete(uri => removeEntriesInFile(uri.fsPath))

/**
* Read the value `intellisense.citation.format`
Expand Down
38 changes: 21 additions & 17 deletions src/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ export const cache = {
}

// Listener for file changes: refreshes the cache if the file can be cached.
lw.watcher.src.onChange((filePath: string) => {
if (canCache(filePath)) {
void refreshCache(filePath)
lw.watcher.src.onChange(uri => {
if (canCache(uri.fsPath)) {
void refreshCache(uri.fsPath)
}
})

// Listener for file deletions: removes the file from the cache if it exists.
lw.watcher.src.onDelete((filePath: string) => {
if (get(filePath) !== undefined) {
caches.delete(filePath)
logger.log(`Removed ${filePath} .`)
lw.watcher.src.onDelete(uri => {
if (get(uri.fsPath) !== undefined) {
caches.delete(uri.fsPath)
logger.log(`Removed ${uri.fsPath} .`)
}
})

Expand Down Expand Up @@ -114,9 +114,10 @@ function add(filePath: string) {
logger.log(`Ignored ${filePath} .`)
return
}
if (!lw.watcher.src.has(filePath)) {
const uri = vscode.Uri.file(filePath)
if (!lw.watcher.src.has(uri)) {
logger.log(`Adding ${filePath} .`)
lw.watcher.src.add(filePath)
lw.watcher.src.add(uri)
}
}

Expand Down Expand Up @@ -393,7 +394,7 @@ async function updateChildrenInput(fileCache: FileCache, rootPath: string) {
})
logger.log(`Input ${result.path} from ${fileCache.filePath} .`)

if (lw.watcher.src.has(result.path)) {
if (lw.watcher.src.has(vscode.Uri.file(result.path))) {
continue
}
add(result.path)
Expand Down Expand Up @@ -440,7 +441,7 @@ async function updateChildrenXr(fileCache: FileCache, rootPath: string) {
logger.log(`External document ${externalPath} from ${fileCache.filePath} .` + (result[1] ? ` Prefix is ${result[1]}`: ''))
}

if (lw.watcher.src.has(externalPath)) {
if (lw.watcher.src.has(vscode.Uri.file(externalPath))) {
continue
}
add(externalPath)
Expand Down Expand Up @@ -506,8 +507,9 @@ function updateBibfiles(fileCache: FileCache) {
}
fileCache.bibfiles.add(bibPath)
logger.log(`Bib ${bibPath} from ${fileCache.filePath} .`)
if (!lw.watcher.bib.has(bibPath)) {
lw.watcher.bib.add(bibPath)
const bibUri = vscode.Uri.file(bibPath)
if (!lw.watcher.bib.has(bibUri)) {
lw.watcher.bib.add(bibUri)
}
}
}
Expand Down Expand Up @@ -544,13 +546,14 @@ async function loadFlsFile(filePath: string): Promise<void> {
const ioFiles = parseFlsContent(await lw.file.read(flsPath) ?? '', rootDir)

for (const inputFile of ioFiles.input) {
const inputUri = vscode.Uri.file(inputFile)
// Drop files that are also listed as OUTPUT or should be ignored
if (ioFiles.output.includes(inputFile) ||
isExcluded(inputFile) ||
!await lw.file.exists(inputFile)) {
continue
}
if (inputFile === filePath || lw.watcher.src.has(inputFile)) {
if (inputFile === filePath || lw.watcher.src.has(inputUri)) {
// Drop the current rootFile often listed as INPUT
// Drop any file that is already watched as it is handled by
// onWatchedFileChange.
Expand All @@ -576,7 +579,7 @@ async function loadFlsFile(filePath: string): Promise<void> {
} else {
logger.log(`Cache not finished on ${filePath} when parsing fls.`)
}
} else if (!lw.watcher.src.has(inputFile) && !['.aux', '.out'].includes(inputExt)) {
} else if (!lw.watcher.src.has(inputUri) && !['.aux', '.out'].includes(inputExt)) {
// Watch non-tex files. aux and out are excluded because they are auto-generated during the building process
add(inputFile)
}
Expand Down Expand Up @@ -672,8 +675,9 @@ async function parseAuxFile(filePath: string, srcDir: string) {
get(lw.root.file.path)?.bibfiles.add(bibPath)
logger.log(`Found .bib ${bibPath} from .aux ${filePath} .`)
}
if (!lw.watcher.bib.has(bibPath)) {
lw.watcher.bib.add(bibPath)
const bibUri = vscode.Uri.file(bibPath)
if (!lw.watcher.bib.has(bibUri)) {
lw.watcher.bib.add(bibUri)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/core/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const root = {
getWorkspace
}

lw.watcher.src.onDelete(filePath => {
if (filePath !== root.file.path) {
lw.watcher.src.onDelete(uri => {
if (uri.fsPath !== root.file.path) {
return
}
root.file = { path: undefined, langId: undefined }
Expand Down
107 changes: 54 additions & 53 deletions src/core/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,27 @@ class Watcher {
private get onCreateHandlers() {
return this._onCreateHandlers
}
private readonly _onCreateHandlers: Set<(filePath: string) => void> = new Set()
private readonly _onCreateHandlers: Set<(uri: vscode.Uri) => void> = new Set()
/**
* Set of handlers to be called when a file is changed.
*/
private get onChangeHandlers() {
return this._onChangeHandlers
}
private readonly _onChangeHandlers: Set<(filePath: string) => void> = new Set()
private readonly _onChangeHandlers: Set<(uri: vscode.Uri) => void> = new Set()
/**
* Set of handlers to be called when a file is deleted.
*/
private get onDeleteHandlers() {
return this._onDeleteHandlers
}
private readonly _onDeleteHandlers: Set<(filePath: string) => void> = new Set()
private readonly _onDeleteHandlers: Set<(uri: vscode.Uri) => void> = new Set()
/**
* Map of file paths to polling information. This may be of particular use
* when large binary files are progressively write to disk, and multiple
* 'change' events are therefore emitted in a short period of time.
*/
private readonly polling: {[filePath: string]: {time: number, size: number}} = {}
private readonly polling: {[uriString: string]: {time: number, size: number}} = {}

/**
* Creates a new Watcher instance.
Expand All @@ -52,27 +52,27 @@ class Watcher {
/**
* Adds a handler for file creation events.
*
* @param {(filePath: string) => void} handler - The handler function.
* @param {(uri: vscode.Uri) => void} handler - The handler function.
*/
onCreate(handler: (filePath: string) => void) {
onCreate(handler: (uri: vscode.Uri) => void) {
this.onCreateHandlers.add(handler)
}

/**
* Adds a handler for file change events.
*
* @param {(filePath: string) => void} handler - The handler function.
* @param {(uri: vscode.Uri) => void} handler - The handler function.
*/
onChange(handler: (filePath: string) => void) {
onChange(handler: (uri: vscode.Uri) => void) {
this.onChangeHandlers.add(handler)
}

/**
* Adds a handler for file deletion events.
*
* @param {(filePath: string) => void} handler - The handler function.
* @param {(uri: vscode.Uri) => void} handler - The handler function.
*/
onDelete(handler: (filePath: string) => void) {
onDelete(handler: (uri: vscode.Uri) => void) {
this.onDeleteHandlers.add(handler)
}

Expand Down Expand Up @@ -108,7 +108,7 @@ class Watcher {

if (!lw.file.hasBinaryExt(path.extname(uri.fsPath))) {
this.handleNonBinaryFileChange(event, uri)
} else if (!this.polling[uri.fsPath]) {
} else if (!this.polling[uri.toString()]) {
await this.initiatePolling(uri)
}
}
Expand All @@ -120,10 +120,10 @@ class Watcher {
* @param {vscode.Uri} uri - The URI of the changed file.
*/
private handleNonBinaryFileChange(event: string, uri: vscode.Uri): void {
const filePath = uri.fsPath
logger.log(`"${event}" emitted on ${filePath}.`)
this.onChangeHandlers.forEach(handler => handler(filePath))
lw.event.fire(lw.event.FileChanged, filePath)
const uriString = uri.toString()
logger.log(`"${event}" emitted on ${uriString}.`)
this.onChangeHandlers.forEach(handler => handler(uri))
lw.event.fire(lw.event.FileChanged, uriString)
}

/**
Expand All @@ -138,14 +138,14 @@ class Watcher {
* @param {vscode.Uri} uri - The URI of the changed file.
*/
private async initiatePolling(uri: vscode.Uri): Promise<void> {
const filePath = uri.fsPath
const uriString = uri.toString()
const firstChangeTime = Date.now()
const size = (await lw.external.stat(vscode.Uri.file(filePath))).size
const size = (await lw.external.stat(uri)).size

this.polling[filePath] = { size, time: firstChangeTime }
this.polling[uriString] = { size, time: firstChangeTime }

const pollingInterval = setInterval(async () => {
await this.handlePolling(filePath, size, firstChangeTime, pollingInterval)
await this.handlePolling(uri, size, firstChangeTime, pollingInterval)
}, vscode.workspace.getConfiguration('latex-workshop').get('latex.watch.pdf.delay') as number)
}

Expand All @@ -159,32 +159,33 @@ class Watcher {
* specified time (200 milliseconds), it is considered a valid change, and
* the appropriate handlers are triggered.
*
* @param {string} filePath - The path of the changed file.
* @param {uri: vscode.Uri} uri - The uri of the changed file.
* @param {number} size - The size of the file.
* @param {number} firstChangeTime - The timestamp of the first change.
* @param {NodeJS.Timeout} interval - The polling interval.
*/
private async handlePolling(filePath: string, size: number, firstChangeTime: number, interval: NodeJS.Timeout): Promise<void> {
if (!await lw.file.exists(filePath)) {
private async handlePolling(uri: vscode.Uri, size: number, firstChangeTime: number, interval: NodeJS.Timeout): Promise<void> {
const uriString = uri.toString()
if (!await lw.file.exists(uri)) {
clearInterval(interval)
delete this.polling[filePath]
delete this.polling[uriString]
return
}

const currentSize = (await lw.external.stat(vscode.Uri.file(filePath))).size
const currentSize = (await lw.external.stat(uri)).size

if (currentSize !== size) {
this.polling[filePath].size = currentSize
this.polling[filePath].time = Date.now()
this.polling[uriString].size = currentSize
this.polling[uriString].time = Date.now()
return
}

if (Date.now() - this.polling[filePath].time >= 200) {
logger.log(`"change" emitted on ${filePath} after polling for ${Date.now() - firstChangeTime} ms.`)
if (Date.now() - this.polling[uriString].time >= 200) {
logger.log(`"change" emitted on ${uriString} after polling for ${Date.now() - firstChangeTime} ms.`)
clearInterval(interval)
delete this.polling[filePath]
this.onChangeHandlers.forEach(handler => handler(filePath))
lw.event.fire(lw.event.FileChanged, filePath)
delete this.polling[uriString]
this.onChangeHandlers.forEach(handler => handler(uri))
lw.event.fire(lw.event.FileChanged, uriString)
}
}

Expand All @@ -202,24 +203,24 @@ class Watcher {
return
}

const filePath = uri.fsPath
logger.log(`"delete" emitted on ${filePath}.`)
const uriString = uri.toString()
logger.log(`"delete" emitted on ${uriString}.`)
return new Promise(resolve => {
setTimeout(async () => {
if (await lw.file.exists(filePath)) {
logger.log(`File deleted and re-created: ${filePath} .`)
if (await lw.file.exists(uri)) {
logger.log(`File deleted and re-created: ${uriString} .`)
resolve()
return
}
logger.log(`File deletion confirmed: ${filePath} .`)
this.onDeleteHandlers.forEach(handler => handler(filePath))
logger.log(`File deletion confirmed: ${uriString} .`)
this.onDeleteHandlers.forEach(handler => handler(uri))
watcherInfo.files.delete(fileName)

if (watcherInfo.files.size === 0) {
this.disposeWatcher(folder)
}

lw.event.fire(lw.event.FileRemoved, filePath)
lw.event.fire(lw.event.FileRemoved, uriString)
resolve()
}, vscode.workspace.getConfiguration('latex-workshop').get('latex.watch.delay') as number)
})
Expand All @@ -246,43 +247,43 @@ class Watcher {
* the set of files being watched. If a watcher already exists, the file is
* simply added to the set of files being watched by the existing watcher.
*
* @param {string} filePath - The path of the file to watch.
* @param {vscode.Uri} uri - The uri of the file to watch.
*/
add(filePath: string) {
const fileName = path.basename(filePath)
const folder = path.dirname(filePath)
add(uri: vscode.Uri) {
const fileName = path.basename(uri.fsPath)
const folder = path.dirname(uri.fsPath)
if (!this.watchers[folder]) {
this.watchers[folder] = {
watcher: this.createWatcher(new vscode.RelativePattern(folder, `*${this.fileExt}`)),
files: new Set([fileName])
}
this.onCreateHandlers.forEach(handler => handler(filePath))
logger.log(`Watched ${filePath} with a new ${this.fileExt} watcher on ${folder} .`)
this.onCreateHandlers.forEach(handler => handler(uri))
logger.log(`Watched ${uri.toString()} with a new ${this.fileExt} watcher on ${folder} .`)
} else {
this.watchers[folder].files.add(fileName)
this.onCreateHandlers.forEach(handler => handler(filePath))
logger.log(`Watched ${filePath} by the ${this.fileExt} watcher.`)
this.onCreateHandlers.forEach(handler => handler(uri))
logger.log(`Watched ${uri.toString()} by the ${this.fileExt} watcher.`)
}
lw.event.fire(lw.event.FileWatched, filePath)
lw.event.fire(lw.event.FileWatched, uri.toString())
}

/**
* Removes a file from being watched.
*
* @param {string} filePath - The path of the file to stop watching.
* @param {vscode.Uri} uri - The uri of the file to stop watching.
*/
remove(filePath: string) {
this.watchers[path.dirname(filePath)]?.files.delete(path.basename(filePath))
remove(uri: vscode.Uri) {
this.watchers[path.dirname(uri.fsPath)]?.files.delete(path.basename(uri.fsPath))
}

/**
* Checks if a file is currently being watched.
*
* @param {string} filePath - The path of the file to check.
* @param {vscode.Uri} uri - The uri of the file to check.
* @returns {boolean} - Indicates whether the file is being watched.
*/
has(filePath: string): boolean {
return this.watchers[path.dirname(filePath)]?.files.has(path.basename(filePath))
has(uri: vscode.Uri): boolean {
return this.watchers[path.dirname(uri.fsPath)]?.files.has(path.basename(uri.fsPath))
}

/**
Expand Down
Loading

0 comments on commit 5b0f7ab

Please sign in to comment.