返回 slidev
annotations.ts
根目录 / packages / vscode / src / views / annotations.ts
1 import type { SourceSlideInfo } from '@slidev/types'
2 import type { DecorationOptions } from 'vscode'
3 import { clamp, ensurePrefix } from '@antfu/utils'
4 import { computed, defineService, useActiveTextEditor, watch } from 'reactive-vscode'
5 import { Position, Range, ThemeColor, window } from 'vscode'
6 import { useProjectFromDoc } from '../composables/useProjectFromDoc'
7 import { config } from '../configs'
8 import { activeProject } from '../projects'
9 import { toRelativePath } from '../utils/toRelativePath'
10
11 const RE_NEWLINE = /\r?\n/
12 const RE_LEADING_BACKTICKS = /^\s*`+/
13 const RE_FRONTMATTER_BLOCK = /^---[\s\S]*?\n---/
14
15 const frontmatterBgOptions = {
16 isWholeLine: true,
17 backgroundColor: '#8881',
18 }
19
20 const firstLineDecoration = window.createTextEditorDecorationType({})
21 const dividerDecoration = window.createTextEditorDecorationType({
22 ...frontmatterBgOptions,
23 color: new ThemeColor('panelTitle.inactiveForeground'),
24 borderStyle: 'solid',
25 borderWidth: '1px 0 0 0',
26 borderColor: new ThemeColor('panelTitle.activeBorder'),
27 })
28 const frontmatterContentDecoration = window.createTextEditorDecorationType(frontmatterBgOptions)
29 const frontmatterEndDecoration = window.createTextEditorDecorationType({
30 ...frontmatterBgOptions,
31 color: new ThemeColor('panelTitle.inactiveForeground'),
32 })
33 const errorDecoration = window.createTextEditorDecorationType({
34 isWholeLine: true,
35 })
36 const codeBlockLineNumberDecoration = window.createTextEditorDecorationType({
37 isWholeLine: false,
38 rangeBehavior: 1,
39 })
40
41 function mergeSlideNumbers(slides: { index: number }[]): string {
42 const indexes = slides.map(s => s.index + 1)
43 const merged = [[indexes[0], indexes[0]]]
44 for (let i = 1; i < indexes.length; i++) {
45 if (merged[merged.length - 1][1] + 1 === indexes[i])
46 merged[merged.length - 1][1] = indexes[i]
47 else
48 merged.push([indexes[i], indexes[i]])
49 }
50 return merged.map(([start, end]) => start === end ? `#${start}` : `#${start}-${end}`).join(', ')
51 }
52
53 interface CodeBlockInfo {
54 startLine: number
55 endLine: number
56 indent: number
57 }
58
59 function findCodeBlocks(docText: string): CodeBlockInfo[] {
60 const lines = docText.split(RE_NEWLINE)
61 const codeBlocks: CodeBlockInfo[] = []
62
63 for (let i = 0; i < lines.length; i++) {
64 const line = lines[i]
65 const trimmedLine = line.trimStart()
66
67 if (trimmedLine.startsWith('```')) {
68 const indent = line.length - trimmedLine.length
69 const codeBlockLevel = line.match(RE_LEADING_BACKTICKS)![0]
70 const backtickCount = codeBlockLevel.trim().length
71 const startLine = i
72
73 if (backtickCount !== 3) {
74 continue
75 }
76
77 let endLine = i
78 for (let j = i + 1; j < lines.length; j++) {
79 if (lines[j].startsWith(codeBlockLevel)) {
80 endLine = j
81 break
82 }
83 }
84
85 if (endLine > startLine) {
86 codeBlocks.push({
87 startLine: startLine + 1,
88 endLine,
89 indent,
90 })
91 }
92
93 i = endLine
94 }
95 }
96
97 return codeBlocks
98 }
99
100 function updateCodeBlockLineNumbers(editor: ReturnType<typeof useActiveTextEditor>['value'], docText: string) {
101 if (!editor || !config['annotations-line-numbers'])
102 return
103
104 const codeBlockLineNumbers: DecorationOptions[] = []
105 const codeBlocks = findCodeBlocks(docText)
106
107 for (const block of codeBlocks) {
108 const lineCount = block.endLine - block.startLine
109 const maxLineNumber = lineCount
110 const numberWidth = String(maxLineNumber).length
111
112 for (let i = 0; i < lineCount; i++) {
113 const lineNumber = i + 1
114 const currentLine = block.startLine + i
115
116 if (currentLine >= editor.document.lineCount)
117 continue
118
119 // \u2800 renders as a space but won't be trimmed
120 const paddedNumber = String(lineNumber).padStart(numberWidth, '\u2800')
121
122 const position = new Position(currentLine, block.indent)
123 codeBlockLineNumbers.push({
124 range: new Range(position, position),
125 renderOptions: {
126 before: {
127 contentText: `${paddedNumber}│`,
128 color: new ThemeColor('editorLineNumber.foreground'),
129 fontWeight: 'normal',
130 fontStyle: 'normal',
131 margin: '0 0.5em 0 0',
132 },
133 },
134 })
135 }
136 }
137
138 editor.setDecorations(codeBlockLineNumberDecoration, codeBlockLineNumbers)
139 }
140
141 export const useAnnotations = defineService(() => {
142 const editor = useActiveTextEditor()
143 const doc = computed(() => editor.value?.document)
144 const projectInfo = useProjectFromDoc(doc)
145
146 watch(
147 [editor, doc, projectInfo, activeProject, () => config.annotations, () => config['annotations-line-numbers']],
148 ([editor, doc, projectInfo, activeProject, enabled, lineNumbersEnabled]) => {
149 if (!editor || !doc)
150 return
151
152 if (!projectInfo || !enabled) {
153 editor.setDecorations(firstLineDecoration, [])
154 editor.setDecorations(dividerDecoration, [])
155 editor.setDecorations(frontmatterContentDecoration, [])
156 editor.setDecorations(frontmatterEndDecoration, [])
157 editor.setDecorations(errorDecoration, [])
158 editor.setDecorations(codeBlockLineNumberDecoration, [])
159 return
160 }
161
162 const { project, md } = projectInfo
163 const isActive = project === activeProject
164 const docText = doc.getText()
165
166 function getTextContent(source: SourceSlideInfo) {
167 const slides = project.data.slides.filter(
168 s => s.source === source || s.importChain?.includes(source),
169 )
170 const posInfo = slides?.length
171 ? mergeSlideNumbers(slides)
172 : isActive ? '(hidden)' : ''
173 const entryInfo = source.index === 0 && project.data.entry !== md
174 ? ` (entry: ${toRelativePath(project.entry)})`
175 : ''
176 const activeInfo = source.index === 0 && !isActive
177 ? ' (inactive)'
178 : ''
179 return ` ${posInfo}${entryInfo}${activeInfo}`
180 }
181
182 const firstLineRanges: DecorationOptions[] = []
183 const dividerRanges: DecorationOptions[] = []
184 const frontmatterContentRanges: DecorationOptions[] = []
185 const frontmatterEndRanges: DecorationOptions[] = []
186 for (const slide of md.slides) {
187 const lineNo = slide.frontmatterStyle === 'frontmatter' ? slide.start : slide.start - 1
188 const line = doc.lineAt(clamp(lineNo, 0, doc.lineCount))
189
190 const start = new Position(line.lineNumber, 0)
191 const startDividerRange = new Range(start, new Position(line.lineNumber, line.text.length))
192
193 // If a markdown has no headmatter, we should set `isWholeLine` to `false` for the first line
194 const isSpecialCase = slide.index === 0 && !slide.frontmatterStyle
195 const ranges = isSpecialCase ? firstLineRanges : dividerRanges
196 ranges.push({
197 range: startDividerRange,
198 renderOptions: {
199 after: {
200 contentText: getTextContent(slide),
201 fontWeight: 'bold',
202 color: new ThemeColor('panelTitle.activeBorder'),
203 },
204 },
205 })
206
207 if (slide.frontmatterRaw != null) {
208 const range = docText.slice(doc.offsetAt(start))
209 const match = range.match(RE_FRONTMATTER_BLOCK)
210 if (match && match.index != null) {
211 const endLine = doc.positionAt(doc.offsetAt(start) + match.index + match[0].length).line
212 frontmatterContentRanges.push({
213 range: new Range(new Position(line.lineNumber + 1, 0), new Position(endLine - 1, 0)),
214 })
215 if (endLine !== start.line) {
216 frontmatterEndRanges.push({
217 range: new Range(new Position(endLine, 0), new Position(endLine, 0)),
218 })
219 }
220 }
221 }
222 }
223
224 editor.setDecorations(firstLineDecoration, firstLineRanges)
225 editor.setDecorations(dividerDecoration, dividerRanges)
226 editor.setDecorations(frontmatterContentDecoration, frontmatterContentRanges)
227 editor.setDecorations(frontmatterEndDecoration, frontmatterEndRanges)
228
229 const errors: DecorationOptions[] = []
230 for (const error of md.errors ?? []) {
231 errors.push({
232 range: new Range(error.row, 0, error.row, 0),
233 renderOptions: {
234 after: {
235 contentText: ensurePrefix(error.message, ' '),
236 color: new ThemeColor('editorError.foreground'),
237 backgroundColor: new ThemeColor('editorError.background'),
238 },
239 },
240 })
241 }
242 editor.setDecorations(errorDecoration, errors)
243
244 if (lineNumbersEnabled) {
245 updateCodeBlockLineNumbers(editor, docText)
246 }
247 else {
248 editor.setDecorations(codeBlockLineNumberDecoration, [])
249 }
250 },
251 { immediate: true },
252 )
253 })
254
254 lines TYPESCRIPT