diff --git a/services/web/app/src/Features/Project/ProjectRootDocManager.mjs b/services/web/app/src/Features/Project/ProjectRootDocManager.mjs
index 59fd4ffcf0..29375590fe 100644
--- a/services/web/app/src/Features/Project/ProjectRootDocManager.mjs
+++ b/services/web/app/src/Features/Project/ProjectRootDocManager.mjs
@@ -51,24 +51,20 @@ async function setRootDocAutomatically(projectId) {
}
async function findRootDocFileFromDirectory(directoryPath) {
- const unsortedFiles = await globby(['**/*.{tex,Rtex,Rnw}'], {
+ // First try LaTeX files (look for \documentclass)
+ const unsortedTexFiles = await globby(['**/*.{tex,Rtex,Rnw,ltx}'], {
cwd: directoryPath,
followSymlinkedDirectories: false,
onlyFiles: true,
case: false,
})
- // the search order is such that we prefer files closer to the project root, then
- // we go by file size in ascending order, because people often have a main
- // file that just includes a bunch of other files; then we go by name, in
- // order to be deterministic
-
- const files = await _sortFileList(unsortedFiles, directoryPath)
- let firstFileInRootFolder
+ const texFiles = await _sortFileList(unsortedTexFiles, directoryPath)
+ let firstTexInRootFolder
let doc = null
- while (files.length > 0 && doc == null) {
- const file = files.shift()
+ while (texFiles.length > 0 && doc == null) {
+ const file = texFiles.shift()
const content = await fs.promises.readFile(
Path.join(directoryPath, file),
'utf8'
@@ -77,18 +73,52 @@ async function findRootDocFileFromDirectory(directoryPath) {
if (DocumentHelper.contentHasDocumentclass(normalizedContent)) {
doc = { path: file, content: normalizedContent }
}
-
- if (!firstFileInRootFolder && !file.includes('/')) {
- firstFileInRootFolder = { path: file, content: normalizedContent }
+ if (!firstTexInRootFolder && !file.includes('/')) {
+ firstTexInRootFolder = { path: file, content: normalizedContent }
}
}
- // if no doc was found, use the first file in the root folder as the main doc
- if (!doc && firstFileInRootFolder) {
- doc = firstFileInRootFolder
+ if (!doc && firstTexInRootFolder) {
+ doc = firstTexInRootFolder
}
- return { path: doc?.path, content: doc?.content }
+ if (doc) return { path: doc.path, content: doc.content }
+
+ // Then try Typst files
+ const typFiles = await globby(['*.typ'], {
+ cwd: directoryPath,
+ followSymlinkedDirectories: false,
+ onlyFiles: true,
+ case: false,
+ })
+ if (typFiles.length > 0) {
+ typFiles.sort()
+ const file = typFiles[0]
+ const content = await fs.promises.readFile(
+ Path.join(directoryPath, file),
+ 'utf8'
+ )
+ return { path: file, content }
+ }
+
+ // Then try Quarto/R Markdown files
+ const qmdFiles = await globby(['*.{qmd,Rmd,rmd}'], {
+ cwd: directoryPath,
+ followSymlinkedDirectories: false,
+ onlyFiles: true,
+ case: false,
+ })
+ if (qmdFiles.length > 0) {
+ qmdFiles.sort()
+ const file = qmdFiles[0]
+ const content = await fs.promises.readFile(
+ Path.join(directoryPath, file),
+ 'utf8'
+ )
+ return { path: file, content }
+ }
+
+ return { path: undefined, content: undefined }
}
async function setRootDocFromName(projectId, rootDocName) {
diff --git a/services/web/app/src/Features/Uploads/ProjectUploadManager.mjs b/services/web/app/src/Features/Uploads/ProjectUploadManager.mjs
index 35add603e5..8b7a528310 100644
--- a/services/web/app/src/Features/Uploads/ProjectUploadManager.mjs
+++ b/services/web/app/src/Features/Uploads/ProjectUploadManager.mjs
@@ -14,10 +14,21 @@ import ProjectRootDocManager from '../Project/ProjectRootDocManager.mjs'
import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
import ProjectDeleter from '../Project/ProjectDeleter.mjs'
import { DeletedProjectReasons } from '../Project/DeletedProjectReasons.mjs'
+import ProjectOptionsHandler from '../Project/ProjectOptionsHandler.mjs'
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.mjs'
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
+const COMPILER_BY_EXTENSION = {
+ tex: 'pdflatex',
+ rtex: 'pdflatex',
+ ltx: 'pdflatex',
+ rnw: 'pdflatex',
+ typ: 'typst',
+ qmd: 'quarto',
+ rmd: 'quarto',
+}
+
export default {
createProjectFromZipArchive: callbackify(createProjectFromZipArchive),
createProjectFromZipArchiveWithName: callbackify(
@@ -52,6 +63,11 @@ async function createProjectFromZipArchive(ownerId, defaultName, zipPath) {
project._id,
path
)
+ const ext = path.split('.').pop()?.toLowerCase()
+ const compiler = ext ? COMPILER_BY_EXTENSION[ext] : null
+ if (compiler) {
+ await ProjectOptionsHandler.promises.setCompiler(project._id, compiler)
+ }
}
} catch (err) {
// no need to wait for the cleanup here
diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx
index 80eb8146c5..c014b20da9 100644
--- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx
+++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.tsx
@@ -14,6 +14,8 @@ import { useFileTreeMainContext } from '../../contexts/file-tree-main'
import { findInTree } from '../../util/find-in-tree'
import useConvertDoc from '@/features/ide-react/hooks/use-convert-doc'
import getMeta from '@/utils/meta'
+import { isValidTeXFile } from '@/main/is-valid-tex-file'
+import { syncRootDocId } from '../../util/sync-mutation'
function FileTreeItemMenuItems() {
const { t } = useTranslation()
@@ -29,12 +31,11 @@ function FileTreeItemMenuItems() {
startUploadingDocOrFile,
downloadPath,
selectedFileName,
- canSetRootDocId,
- setRootDocId,
} = useFileTreeActionable()
- const { project } = useProjectContext()
+ const { project, projectId, updateProject } = useProjectContext()
const projectOwner = project?.owner?._id
+ const rootDocId = project?.rootDocId
const { fileTreeData, fileTreeReadOnly } = useFileTreeData()
const { selectedEntityIds } = useFileTreeSelectable()
@@ -42,7 +43,7 @@ function FileTreeItemMenuItems() {
const selectedEntityId =
selectedEntityIds.size === 1 ? Array.from(selectedEntityIds)[0] : null
- // Use context-menu-target entity for convert; falls back to selection
+ // Use context-menu-target entity for convert/set-as-main; falls back to selection
const convertEntityId = contextMenuEntityId ?? selectedEntityId
const convertEntity = convertEntityId
? findInTree(fileTreeData, convertEntityId)
@@ -64,6 +65,20 @@ function FileTreeItemMenuItems() {
isConvertableDoc &&
convertEntityName?.endsWith('.typ')
+ const canShowSetAsMain =
+ !fileTreeReadOnly &&
+ isConvertableDoc &&
+ !!convertEntityId &&
+ convertEntityId !== rootDocId &&
+ !!convertEntityName &&
+ isValidTeXFile(convertEntityName)
+
+ const handleSetAsMain = useCallback(async () => {
+ if (!convertEntityId) return
+ await syncRootDocId(projectId, convertEntityId)
+ updateProject({ rootDocId: convertEntityId })
+ }, [convertEntityId, projectId, updateProject])
+
const { convert: convertToTypst } = useConvertDoc('typst', convertEntityId)
const { convert: convertToLatex } = useConvertDoc('latex', convertEntityId)
@@ -102,11 +117,11 @@ function FileTreeItemMenuItems() {
) : null}
- {canSetRootDocId ? (
+ {canShowSetAsMain ? (
<>