From e8809adc26208610bac4158e737e3a391286412b Mon Sep 17 00:00:00 2001 From: zede Date: Wed, 29 Oct 2025 18:03:40 +0500 Subject: [PATCH 1/5] feat: show line numbers in markdown blocks --- packages/vscode/package.json | 6 ++ packages/vscode/src/configs.ts | 2 + packages/vscode/src/views/annotations.ts | 103 ++++++++++++++++++++- packages/vscode/syntaxes/slidev.example.md | 21 +++++ 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index ac7fed8a2f..8f43ad5525 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -363,6 +363,12 @@ "description": "Display annotations for slides markdown files", "default": true }, + "slidev.annotations-line-numbers": { + "type": "boolean", + "scope": "window", + "description": "Display line numbers in code blocks", + "default": true + }, "slidev.preview-sync": { "type": "boolean", "scope": "window", diff --git a/packages/vscode/src/configs.ts b/packages/vscode/src/configs.ts index 4d6d2841da..a544cefd7f 100644 --- a/packages/vscode/src/configs.ts +++ b/packages/vscode/src/configs.ts @@ -5,6 +5,7 @@ export const { 'force-enabled': forceEnabled, 'port': configuredPortInitial, 'annotations': displayAnnotations, + 'annotations-line-numbers': displayCodeBlockLineNumbers, 'preview-sync': previewSyncInitial, include, exclude, @@ -13,6 +14,7 @@ export const { 'force-enabled': Boolean, 'port': Number, 'annotations': Boolean, + 'annotations-line-numbers': Boolean, 'preview-sync': Boolean, 'include': Object as ConfigType, 'exclude': String, diff --git a/packages/vscode/src/views/annotations.ts b/packages/vscode/src/views/annotations.ts index 8b0880c36b..968b9b6a07 100644 --- a/packages/vscode/src/views/annotations.ts +++ b/packages/vscode/src/views/annotations.ts @@ -4,7 +4,7 @@ import { clamp, ensurePrefix } from '@antfu/utils' import { computed, createSingletonComposable, useActiveTextEditor, watch } from 'reactive-vscode' import { Position, Range, ThemeColor, window } from 'vscode' import { useProjectFromDoc } from '../composables/useProjectFromDoc' -import { displayAnnotations } from '../configs' +import { displayAnnotations, displayCodeBlockLineNumbers } from '../configs' import { activeProject } from '../projects' import { toRelativePath } from '../utils/toRelativePath' @@ -30,6 +30,10 @@ const frontmatterEndDecoration = window.createTextEditorDecorationType(dividerCo const errorDecoration = window.createTextEditorDecorationType({ isWholeLine: true, }) +const codeBlockLineNumberDecoration = window.createTextEditorDecorationType({ + isWholeLine: false, + rangeBehavior: 1, +}) function mergeSlideNumbers(slides: { index: number }[]): string { const indexes = slides.map(s => s.index + 1) @@ -40,7 +44,54 @@ function mergeSlideNumbers(slides: { index: number }[]): string { else merged.push([indexes[i], indexes[i]]) } - return merged.map(([start, end]) => start === end ? `#${start}` : `#${start}-${end}`).join(', ') + return merged.map(([start, end]) => start === end ? `#@${start}` : `#${start}-${end}`).join(', ') +} + +interface CodeBlockInfo { + startLine: number + endLine: number + indent: string +} + +function findCodeBlocks(docText: string): CodeBlockInfo[] { + const lines = docText.split(/\r?\n/) + const codeBlocks: CodeBlockInfo[] = [] + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const trimmedLine = line.trimStart() + + if (trimmedLine.startsWith('```')) { + const indent = line.slice(0, line.length - trimmedLine.length) + const codeBlockLevel = line.match(/^\s*`+/)![0] + const backtickCount = codeBlockLevel.trim().length + const startLine = i + + if (backtickCount !== 3) { + continue + } + + let endLine = i + for (let j = i + 1; j < lines.length; j++) { + if (lines[j].startsWith(codeBlockLevel)) { + endLine = j + break + } + } + + if (endLine > startLine) { + codeBlocks.push({ + startLine: startLine + 1, + endLine, + indent, + }) + } + + i = endLine + } + } + + return codeBlocks } export const useAnnotations = createSingletonComposable(() => { @@ -48,8 +99,8 @@ export const useAnnotations = createSingletonComposable(() => { const doc = computed(() => editor.value?.document) const projectInfo = useProjectFromDoc(doc) watch( - [editor, doc, projectInfo, activeProject, displayAnnotations], - ([editor, doc, projectInfo, activeProject, enabled]) => { + [editor, doc, projectInfo, activeProject, displayAnnotations, displayCodeBlockLineNumbers], + ([editor, doc, projectInfo, activeProject, enabled, lineNumbersEnabled]) => { if (!editor || !doc || !projectInfo) return @@ -58,6 +109,7 @@ export const useAnnotations = createSingletonComposable(() => { editor.setDecorations(dividerDecoration, []) editor.setDecorations(frontmatterContentDecoration, []) editor.setDecorations(errorDecoration, []) + editor.setDecorations(codeBlockLineNumberDecoration, []) return } @@ -141,6 +193,49 @@ export const useAnnotations = createSingletonComposable(() => { }) } editor.setDecorations(errorDecoration, errors) + + if (lineNumbersEnabled) { + const codeBlockLineNumbers: DecorationOptions[] = [] + const codeBlocks = findCodeBlocks(docText) + + for (const block of codeBlocks) { + const lineCount = block.endLine - block.startLine + + const maxLineNumber = lineCount + const numberWidth = String(maxLineNumber).length + + for (let i = 0; i < lineCount; i++) { + const lineNumber = i + 1 + const currentLine = block.startLine + i + + if (currentLine >= doc.lineCount) + continue + + const paddedNumber = String(lineNumber).padStart(numberWidth, ' ') + + codeBlockLineNumbers.push({ + range: new Range( + new Position(currentLine, 0), + new Position(currentLine, 0), + ), + renderOptions: { + before: { + contentText: `${paddedNumber}│ `, + color: new ThemeColor('editorLineNumber.foreground'), + fontWeight: 'normal', + fontStyle: 'normal', + margin: '0 0.5em 0 0', + }, + }, + }) + } + } + + editor.setDecorations(codeBlockLineNumberDecoration, codeBlockLineNumbers) + } + else { + editor.setDecorations(codeBlockLineNumberDecoration, []) + } }, { immediate: true }, ) diff --git a/packages/vscode/syntaxes/slidev.example.md b/packages/vscode/syntaxes/slidev.example.md index 021c9c2a97..6ccd261358 100644 --- a/packages/vscode/syntaxes/slidev.example.md +++ b/packages/vscode/syntaxes/slidev.example.md @@ -61,8 +61,29 @@ const a = 1 ```ts {monaco-run}{showOutputAt: '+1'} twoslash const a = 1 +const b = 2 ``` $$ \lambda = 1 $$ + +--- +layout: center +text: 2 +--- + +# Magic Move + +````md magic-move +```ts +const a = 1 +``` + +```ts +const a = 1 +const b = 2 +``` +```` + +--- \ No newline at end of file From 19befdb743bc0c0bb8acbbbaa944caccbba4bc21 Mon Sep 17 00:00:00 2001 From: zede Date: Thu, 30 Oct 2025 16:27:09 +0500 Subject: [PATCH 2/5] fix: linting for slidev.example.md --- packages/vscode/syntaxes/slidev.example.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/vscode/syntaxes/slidev.example.md b/packages/vscode/syntaxes/slidev.example.md index 6ccd261358..55897bbedf 100644 --- a/packages/vscode/syntaxes/slidev.example.md +++ b/packages/vscode/syntaxes/slidev.example.md @@ -85,5 +85,3 @@ const a = 1 const b = 2 ``` ```` - ---- \ No newline at end of file From ec653bf4f8580bccdd7c3ed9e21cf8a533c05c49 Mon Sep 17 00:00:00 2001 From: zede Date: Thu, 30 Oct 2025 17:30:57 +0500 Subject: [PATCH 3/5] fix: remove debug marker --- packages/vscode/src/views/annotations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode/src/views/annotations.ts b/packages/vscode/src/views/annotations.ts index 968b9b6a07..097eb735e0 100644 --- a/packages/vscode/src/views/annotations.ts +++ b/packages/vscode/src/views/annotations.ts @@ -44,7 +44,7 @@ function mergeSlideNumbers(slides: { index: number }[]): string { else merged.push([indexes[i], indexes[i]]) } - return merged.map(([start, end]) => start === end ? `#@${start}` : `#${start}-${end}`).join(', ') + return merged.map(([start, end]) => start === end ? `#${start}` : `#${start}-${end}`).join(', ') } interface CodeBlockInfo { From 49f6b5681f390db97b962863a303567a678cd4ad Mon Sep 17 00:00:00 2001 From: zede Date: Mon, 17 Nov 2025 13:52:43 +0500 Subject: [PATCH 4/5] fix: row numbers rendering in code blocks - Fix incorrect output until document is saved - Fix wrong line numbers for rows with 2+ digits - Fix rendering of lines containing only whitespace --- packages/vscode/src/views/annotations.ts | 118 ++++++++++++++------- packages/vscode/syntaxes/slidev.example.md | 8 ++ 2 files changed, 86 insertions(+), 40 deletions(-) diff --git a/packages/vscode/src/views/annotations.ts b/packages/vscode/src/views/annotations.ts index 097eb735e0..9afc09dfc9 100644 --- a/packages/vscode/src/views/annotations.ts +++ b/packages/vscode/src/views/annotations.ts @@ -1,8 +1,8 @@ import type { SourceSlideInfo } from '@slidev/types' import type { DecorationOptions } from 'vscode' -import { clamp, ensurePrefix } from '@antfu/utils' -import { computed, createSingletonComposable, useActiveTextEditor, watch } from 'reactive-vscode' -import { Position, Range, ThemeColor, window } from 'vscode' +import { clamp, debounce, ensurePrefix } from '@antfu/utils' +import { computed, createSingletonComposable, onScopeDispose, useActiveTextEditor, watch } from 'reactive-vscode' +import { Position, Range, ThemeColor, window, workspace } from 'vscode' import { useProjectFromDoc } from '../composables/useProjectFromDoc' import { displayAnnotations, displayCodeBlockLineNumbers } from '../configs' import { activeProject } from '../projects' @@ -94,10 +94,84 @@ function findCodeBlocks(docText: string): CodeBlockInfo[] { return codeBlocks } +function updateCodeBlockLineNumbers(editor: ReturnType['value'], docText: string) { + if (!editor || !displayCodeBlockLineNumbers.value) + return + + const codeBlockLineNumbers: DecorationOptions[] = [] + const codeBlocks = findCodeBlocks(docText) + + for (const block of codeBlocks) { + const lineCount = block.endLine - block.startLine + const maxLineNumber = lineCount + const numberWidth = String(maxLineNumber).length + + for (let i = 0; i < lineCount; i++) { + const lineNumber = i + 1 + const currentLine = block.startLine + i + + if (currentLine >= editor.document.lineCount) + continue + + const paddedNumber = String(lineNumber).padStart(numberWidth, '⠀') + + codeBlockLineNumbers.push({ + range: new Range( + new Position(currentLine, 0), + new Position(currentLine, 0), + ), + renderOptions: { + before: { + contentText: `${paddedNumber}│`, + color: new ThemeColor('editorLineNumber.foreground'), + fontWeight: 'normal', + fontStyle: 'normal', + margin: '0 0.5em 0 0', + }, + }, + }) + } + } + + editor.setDecorations(codeBlockLineNumberDecoration, codeBlockLineNumbers) +} + export const useAnnotations = createSingletonComposable(() => { const editor = useActiveTextEditor() const doc = computed(() => editor.value?.document) const projectInfo = useProjectFromDoc(doc) + + let debouncedUpdateLineNumbers: ((docText: string) => void) | null = null + + watch( + [editor, displayCodeBlockLineNumbers], + ([currentEditor, lineNumbersEnabled]) => { + debouncedUpdateLineNumbers = null + + if (!currentEditor || !lineNumbersEnabled) { + if (currentEditor) + currentEditor.setDecorations(codeBlockLineNumberDecoration, []) + return + } + + debouncedUpdateLineNumbers = debounce(150, (docText: string) => { + if (editor.value === currentEditor) + updateCodeBlockLineNumbers(currentEditor, docText) + }) + }, + { immediate: true }, + ) + + const textChangeDisposable = workspace.onDidChangeTextDocument((e) => { + if (editor.value?.document === e.document && displayCodeBlockLineNumbers.value && debouncedUpdateLineNumbers) { + debouncedUpdateLineNumbers(e.document.getText()) + } + }) + + onScopeDispose(() => { + textChangeDisposable.dispose() + }) + watch( [editor, doc, projectInfo, activeProject, displayAnnotations, displayCodeBlockLineNumbers], ([editor, doc, projectInfo, activeProject, enabled, lineNumbersEnabled]) => { @@ -195,43 +269,7 @@ export const useAnnotations = createSingletonComposable(() => { editor.setDecorations(errorDecoration, errors) if (lineNumbersEnabled) { - const codeBlockLineNumbers: DecorationOptions[] = [] - const codeBlocks = findCodeBlocks(docText) - - for (const block of codeBlocks) { - const lineCount = block.endLine - block.startLine - - const maxLineNumber = lineCount - const numberWidth = String(maxLineNumber).length - - for (let i = 0; i < lineCount; i++) { - const lineNumber = i + 1 - const currentLine = block.startLine + i - - if (currentLine >= doc.lineCount) - continue - - const paddedNumber = String(lineNumber).padStart(numberWidth, ' ') - - codeBlockLineNumbers.push({ - range: new Range( - new Position(currentLine, 0), - new Position(currentLine, 0), - ), - renderOptions: { - before: { - contentText: `${paddedNumber}│ `, - color: new ThemeColor('editorLineNumber.foreground'), - fontWeight: 'normal', - fontStyle: 'normal', - margin: '0 0.5em 0 0', - }, - }, - }) - } - } - - editor.setDecorations(codeBlockLineNumberDecoration, codeBlockLineNumbers) + updateCodeBlockLineNumbers(editor, docText) } else { editor.setDecorations(codeBlockLineNumberDecoration, []) diff --git a/packages/vscode/syntaxes/slidev.example.md b/packages/vscode/syntaxes/slidev.example.md index 55897bbedf..a2a99bc0dc 100644 --- a/packages/vscode/syntaxes/slidev.example.md +++ b/packages/vscode/syntaxes/slidev.example.md @@ -83,5 +83,13 @@ const a = 1 ```ts const a = 1 const b = 2 +const c = 3 + + + + + + +1 ``` ```` From 387fdbd7d87814ae352367680c5fc3336aeb24d8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:54:08 +0000 Subject: [PATCH 5/5] [autofix.ci] apply automated fixes --- packages/vscode/syntaxes/slidev.example.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/vscode/syntaxes/slidev.example.md b/packages/vscode/syntaxes/slidev.example.md index a2a99bc0dc..309a044bae 100644 --- a/packages/vscode/syntaxes/slidev.example.md +++ b/packages/vscode/syntaxes/slidev.example.md @@ -85,11 +85,6 @@ const a = 1 const b = 2 const c = 3 - - - - - 1 ``` ````