返回 slidev
useFocusedSlide.ts
根目录 / packages / vscode / src / composables / useFocusedSlide.ts
1 import type { SlidevProject } from '../projects'
2 import { computed, defineService, useActiveTextEditor, useTextEditorSelection, useTextEditorVisibleRanges, useVscodeContext } from 'reactive-vscode'
3 import { Position, Range, TextEditorRevealType, Uri, window, workspace } from 'vscode'
4 import { activeData } from '../projects'
5 import { getFirstDisplayedChild } from '../utils/getFirstDisplayedChild'
6 import { useDebouncedComputed } from './useDebouncedComputed'
7 import { useProjectFromDoc } from './useProjectFromDoc'
8
9 interface OverviewAnchor {
10 line: number
11 no: number
12 }
13
14 export const useFocusedSlide = defineService(() => {
15 const editor = useActiveTextEditor()
16 const selection = useTextEditorSelection(editor)
17 const visibleRanges = useTextEditorVisibleRanges(editor)
18 const debouncedEditor = useDebouncedComputed(() => editor.value, val => val ? null : 150)
19 const projectInfo = useProjectFromDoc(() => debouncedEditor.value?.document)
20 const focusedMarkdown = computed(() => projectInfo.value?.md)
21 useVscodeContext('slidev:editing-markdown', () => !!focusedMarkdown.value)
22
23 const focusedSourceSlide = useDebouncedComputed(
24 () => {
25 const md = focusedMarkdown.value
26 if (!md || !debouncedEditor.value || !selection.value) {
27 return null
28 }
29 const line = selection.value.active.line + 1
30 const slide = md.slides.find(s => line <= s.end)
31 return slide || md.slides.at(-1)!
32 },
33 (newVal, oldVal) => newVal?.filepath === oldVal?.filepath ? 30 : 150,
34 )
35
36 const displayedSourceSlide = computed(() => {
37 if (!focusedSourceSlide.value)
38 return null
39 return getFirstDisplayedChild(focusedSourceSlide.value)
40 })
41 const focusedSlideNo = computed(() => {
42 const slides = projectInfo.value?.project.data.slides
43 if (!slides || !displayedSourceSlide.value)
44 return null
45 const index = slides.findIndex(s => s.source === displayedSourceSlide.value)
46 return index < 0 ? null : index + 1
47 })
48 const overviewAnchors = computed(() => {
49 const info = projectInfo.value
50 if (!info)
51 return []
52 const slides = info.project.data.slides
53 return info.md.slides
54 .map((source): OverviewAnchor | null => {
55 const displayedSource = getFirstDisplayedChild(source)
56 const index = slides.findIndex(s => s.source === displayedSource)
57 if (index < 0)
58 return null
59 return {
60 line: source.contentStart,
61 no: index + 1,
62 }
63 })
64 .filter((anchor): anchor is OverviewAnchor => anchor != null)
65 .sort((a, b) => a.no - b.no)
66 })
67 const viewportSlideNo = computed(() => {
68 const range = visibleRanges.value[0]
69 if (!range)
70 return null
71 const start = getFractionalVisibleLine(range.start)
72 const end = getFractionalVisibleLine(range.end)
73 const visibleLines = Math.max(1, end - start)
74 return interpolate(overviewAnchors.value.map(anchor => ({
75 x: (anchor.line - start) / visibleLines,
76 y: anchor.no,
77 })), 0.5)
78 })
79
80 function getFractionalVisibleLine(position: Position) {
81 const document = editor.value?.document
82 const line = position.line
83 if (!document || line < 0 || line >= document.lineCount)
84 return line
85 return line + position.character / (document.lineAt(line).text.length + 2)
86 }
87
88 function interpolate(points: { x: number, y: number }[], x: number) {
89 if (points.length === 0)
90 return null
91 if (points.length === 1)
92 return points[0].y
93 const sorted = [...points].sort((a, b) => a.x - b.x)
94 const minY = Math.min(...sorted.map(point => point.y))
95 const maxY = Math.max(...sorted.map(point => point.y))
96
97 let previous = sorted[0]
98 let next = sorted[1]
99 if (x > sorted.at(-1)!.x) {
100 previous = sorted.at(-2)!
101 next = sorted.at(-1)!
102 }
103 else {
104 for (let i = 1; i < sorted.length; i++) {
105 if (sorted[i].x >= x) {
106 previous = sorted[i - 1]
107 next = sorted[i]
108 break
109 }
110 }
111 }
112
113 const span = next.x - previous.x
114 const y = span === 0
115 ? previous.y
116 : previous.y + (x - previous.x) / span * (next.y - previous.y)
117 return Math.min(Math.max(y, minY), maxY)
118 }
119
120 async function gotoSlide(filepath: string, index: number) {
121 if (focusedMarkdown.value?.filepath === filepath && focusedSourceSlide.value?.index === index)
122 return
123 const slide = activeData.value?.markdownFiles[filepath]?.slides[index]
124 if (!slide)
125 return
126 const document = await workspace.openTextDocument(Uri.file(filepath))
127 const cursorPos = new Position(slide.contentStart, 0)
128 await window.showTextDocument(document, {
129 selection: new Range(cursorPos, cursorPos),
130 })
131 }
132
133 async function focusSlide(project: SlidevProject, no: number) {
134 const source = project.data.slides[no - 1]?.source
135 if (!source || displayedSourceSlide.value === source)
136 return
137 await gotoSlide(source.filepath, source.index)
138 }
139
140 function revealViewportSlide(no: number) {
141 const textEditor = editor.value
142 if (!textEditor)
143 return
144 const line = interpolate(overviewAnchors.value.map(anchor => ({
145 x: anchor.no,
146 y: anchor.line,
147 })), no)
148 if (line == null)
149 return
150 const position = new Position(Math.round(line), 0)
151 textEditor.revealRange(new Range(position, position), TextEditorRevealType.InCenter)
152 }
153
154 return {
155 focusedMarkdown,
156 focusedSourceSlide,
157 focusedSlideNo,
158 viewportSlideNo,
159 gotoSlide,
160 focusSlide,
161 revealViewportSlide,
162 }
163 })
164
164 lines TYPESCRIPT