Skip to content

Commit 47e1424

Browse files
authored
feat(vscode): slidev.include configuration (#1675)
1 parent 8dce420 commit 47e1424

File tree

7 files changed

+63
-39
lines changed

7 files changed

+63
-39
lines changed

docs/guide/editors.md

+8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ In the **preview webview**, you can click the <codicon-run-all /> icon to start
6565

6666
There are also some **commands** you can use. Type `Slidev` in the command palette to see them.
6767

68+
You can add glob patterns to the `slidev.include` configuration to include files as Slidev entries. The default value is `["**/*.md"]`. Example:
69+
70+
```json
71+
{
72+
"slidev.include": ["**/presentation.md"]
73+
}
74+
```
75+
6876
## Prettier Plugin
6977

7078
Slidev also provides a Prettier plugin to format your slides. You can use it with your favorite editor that supports Prettier. Docs for the plugin can be found [here](https://github.com/slidevjs/prettier-plugin).

packages/vscode/package.json

+15
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,21 @@
284284
"scope": "window",
285285
"description": "Sync preview window with editor cursor",
286286
"default": true
287+
},
288+
"slidev.include": {
289+
"type": "array",
290+
"scope": "window",
291+
"description": "Glob patterns to include slides entries",
292+
"items": {
293+
"type": "string"
294+
},
295+
"default": ["**/slides.md"]
296+
},
297+
"slidev.exclude": {
298+
"type": "string",
299+
"scope": "window",
300+
"description": "A glob pattern to exclude slides entries",
301+
"default": "**/node_modules/**"
287302
}
288303
}
289304
},

packages/vscode/src/commands.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { relative } from 'node:path'
12
import { onScopeDispose } from '@vue/runtime-core'
23
import type { Disposable } from 'vscode'
34
import { Position, Range, Selection, TextEditorRevealType, Uri, commands, window, workspace } from 'vscode'
45
import { save as saveSlidevMarkdown } from '@slidev/parser/fs'
6+
import { slash } from '@antfu/utils'
57
import { useDevServer } from './composables/useDevServer'
68
import { useEditingSlideSource } from './composables/useEditingSlideSource'
79
import { useFocusedSlideNo } from './composables/useFocusedSlideNo'
8-
import { configuredPort, forceEnabled, previewSync } from './configs'
10+
import { configuredPort, forceEnabled, include, previewSync } from './configs'
911
import type { SlidevProject } from './projects'
1012
import { activeEntry, activeProject, activeSlidevData, addProject, projects, rescanProjects } from './projects'
1113
import { findPossibleEntries } from './utils/findPossibleEntries'
@@ -41,6 +43,11 @@ export function useCommands() {
4143
if (selected) {
4244
for (const entry of selected)
4345
await addProject(entry)
46+
if (workspace.workspaceFolders) {
47+
const workspaceRoot = workspace.workspaceFolders[0].uri.fsPath
48+
const relatives = selected.map(s => slash(relative(workspaceRoot, s)))
49+
include.value = [...include.value, ...relatives]
50+
}
4451
}
4552
})
4653

packages/vscode/src/configs.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isDeepEqual } from '@antfu/utils'
12
import type { Ref, ShallowRef } from '@vue/runtime-core'
23
import { ref, shallowRef, watch } from '@vue/runtime-core'
34
import type { ColorTheme, ConfigurationChangeEvent } from 'vscode'
@@ -9,15 +10,21 @@ const configKeys: Record<string, ShallowRef<unknown>> = {}
910

1011
function useConfiguration<T>(key: string, defaultValue: T, writeBack = false): Ref<T> {
1112
const r = configKeys[key] = shallowRef<T>(config.get<T>(key) ?? defaultValue)
12-
if (writeBack)
13-
watch(r, v => config.update(key, v))
13+
if (writeBack) {
14+
watch(r, (v) => {
15+
if (!isDeepEqual(v, config.get<T>(key)))
16+
config.update(key, v)
17+
})
18+
}
1419
return r
1520
}
1621

1722
export const forceEnabled = useConfiguration<boolean | null>('force-enabled', null, true)
1823
export const configuredPort = useConfiguration('port', 3030)
1924
export const displayAnnotations = useConfiguration('annotations', true)
2025
export const previewSync = useConfiguration('preview-sync', true)
26+
export const include = useConfiguration<string[]>('include', ['**/slides.md'], true)
27+
export const exclude = useConfiguration<string>('exclude', '**/node_modules/**')
2128

2229
export const isDarkTheme = ref(true)
2330

packages/vscode/src/projects.ts

+22-34
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { existsSync } from 'node:fs'
22
import { basename, dirname } from 'node:path'
33
import type { LoadedSlidevData } from '@slidev/parser/fs'
44
import { load } from '@slidev/parser/fs'
5-
import { computed, markRaw, onScopeDispose, reactive, ref, watchEffect } from '@vue/runtime-core'
6-
import { commands, window, workspace } from 'vscode'
5+
import { computed, markRaw, onScopeDispose, reactive, ref, watch, watchEffect } from '@vue/runtime-core'
6+
import { window, workspace } from 'vscode'
77
import { slash } from '@antfu/utils'
88
import { useLogger } from './views/logger'
99
import { findShallowestPath } from './utils/findShallowestPath'
1010
import { useVscodeContext } from './composables/useVscodeContext'
11-
import { forceEnabled } from './configs'
11+
import { exclude, forceEnabled, include } from './configs'
1212

1313
export interface SlidevProject {
1414
readonly entry: string
@@ -23,17 +23,21 @@ export const activeProject = computed(() => activeEntry.value ? projects.get(act
2323
export const activeSlidevData = computed(() => activeProject.value?.data)
2424
export const activeUserRoot = computed(() => activeProject.value?.userRoot)
2525

26-
async function loadExistingProjects() {
27-
const files = await workspace.findFiles('**/*.md', '**/node_modules/**')
26+
async function addExistingProjects() {
27+
const files = new Set<string>()
28+
for (const glob of include.value) {
29+
(await workspace.findFiles(glob, exclude.value))
30+
.forEach(file => files.add(file.fsPath))
31+
}
2832
for (const file of files) {
29-
const path = slash(file.fsPath)
30-
if (basename(path) === 'slides.md')
33+
const path = slash(file)
34+
if (!projects.has(path))
3135
(await addProjectEffect(path))()
3236
}
3337
}
3438

3539
export async function rescanProjects() {
36-
await loadExistingProjects()
40+
await addExistingProjects()
3741
for (const project of projects.values()) {
3842
if (!existsSync(project.entry)) {
3943
projects.delete(project.entry)
@@ -48,7 +52,7 @@ export function useProjects() {
4852
const logger = useLogger()
4953

5054
async function init() {
51-
await loadExistingProjects()
55+
await addExistingProjects()
5256
await autoSetActiveEntry()
5357
}
5458
init()
@@ -69,55 +73,39 @@ export function useProjects() {
6973

7074
let pendingUpdate: { cancelled: boolean } | null = null
7175

72-
// TODO: Not sure why file creation is not being detected
7376
const fsWatcher = workspace.createFileSystemWatcher('**/*.md')
77+
onScopeDispose(() => fsWatcher.dispose())
78+
7479
fsWatcher.onDidChange(async (uri) => {
75-
const path = slash(uri.fsPath)
80+
const path = slash(uri.fsPath).toLowerCase()
7681
logger.info(`File ${path} changed.`)
7782
const startMs = Date.now()
7883
pendingUpdate && (pendingUpdate.cancelled = true)
7984
const thisUpdate = pendingUpdate = { cancelled: false }
8085
const effects: (() => void)[] = []
81-
let maybeNewEntry = path.endsWith('.md') && basename(path).toLowerCase() !== 'readme.md'
8286
for (const project of projects.values()) {
83-
if (project.data.watchFiles.includes(path))
84-
maybeNewEntry = false
85-
else
87+
if (!project.data.watchFiles.some(f => f.toLowerCase() === path))
8688
continue
8789

8890
if (existsSync(project.entry)) {
8991
const newData = markRaw(await load(project.userRoot, project.entry))
90-
maybeNewEntry &&= newData.watchFiles.includes(path)
9192
effects.push(() => {
9293
project.data = newData
9394
logger.info(`Project ${project.entry} updated.`)
9495
})
9596
}
96-
else {
97-
effects.push(() => {
98-
projects.delete(project.entry)
99-
logger.info(`Project ${project.entry} removed.`)
100-
if (activeEntry.value === project.entry) {
101-
window.showWarningMessage('The active slides file has been deleted. Please choose another one.', 'Choose another one')
102-
.then(result => result && commands.executeCommand('slidev.choose-entry'))
103-
}
104-
})
105-
}
97+
10698
if (thisUpdate.cancelled)
10799
return
108100
}
109101

110-
if (basename(path).toLocaleLowerCase() === 'slides.md' && !projects.has(path))
111-
effects.push(await addProjectEffect(path))
112-
113-
if (thisUpdate.cancelled)
114-
return
115-
116102
effects.map(effect => effect())
117-
autoSetActiveEntry()
118103
logger.info(`All affected Slidev projects updated in ${Date.now() - startMs}ms.`)
119104
})
120-
onScopeDispose(() => fsWatcher.dispose())
105+
fsWatcher.onDidCreate(rescanProjects)
106+
fsWatcher.onDidDelete(rescanProjects)
107+
108+
watch(include, rescanProjects)
121109
}
122110

123111
export async function addProject(entry: string) {

packages/vscode/src/utils/findPossibleEntries.ts

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export async function findPossibleEntries() {
99
.map(uri => slash(uri.fsPath))
1010
.filter(path => !projects.has(path))
1111
.filter(path => ![
12-
'readme.md',
1312
'code_of_conduct.md',
1413
'contributing.md',
1514
'license.md',

packages/vscode/src/views/projectsTree.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const useProjectsTree = createSingletonComposable(() => {
5858
return element
5959
? typeof element === 'string'
6060
? undefined
61-
: element.data.watchFiles.filter(file => file !== element.entry)
61+
: element.data.watchFiles.filter(file => file.toLowerCase() !== element.entry.toLowerCase())
6262
: [...projects.values()]
6363
},
6464
},

0 commit comments

Comments
 (0)