Skip to content

Commit f898302

Browse files
committed
force document reparse on paste due to possible structural hierarchy changes, remove profile logging code
1 parent fdb9100 commit f898302

6 files changed

Lines changed: 195 additions & 196 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ All notable changes to the "pointblank" extension will be documented in this fil
88
- When typing or pasting from the start of a list item line, the bullet point style now matches the next or previous list item line at the same level of indent for consistency.
99
### Changed
1010
- Replaced the document parser with a high-performance "Dirty Range" incremental parser, eliminating typing lag in large documents.
11+
- Paste operations are now handled as a single, atomic transaction. This prevents potential race conditions with the incremental parser and ensures that the document state remains consistent, improving performance and reliability, especially for large pastes.
1112
### Fixed
13+
- Fixed an issue where automatic bullet insertion would interfere with VS Code's auto-completion for brackets, parentheses, and quotes.
1214
- Pasting a bullet at the beginning of a line no longer erases the line; pasted content is merged or inserted intelligently.
1315
- Numbered list items will remove their default bullet point.
1416
- Paste handling is now more robust: bullets are correctly added when pasting at the start of indented lines, and never in the middle of a line.
1517
### Testing
16-
- Added a comprehensive unit test for paste scenarios.
18+
- Added comprehensive unit tests for document parsing and paste scenarios.
1719
- All parsing and paste logic is now tested using Jest unit tests.
1820

1921
## [0.6.3] - 2025-06-27

src/commands/commandManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export class CommandManager {
243243
vscode.commands.executeCommand('outdentLines');
244244
});
245245

246-
const pasteWithBulletsInstance = new PasteWithBullets();
246+
const pasteWithBulletsInstance = new PasteWithBullets(this.extensionState);
247247
const pasteWithBulletsCommand = vscode.commands.registerTextEditorCommand('pointblank.pasteWithBullets', async () => {
248248
if (!config.getAutoBullets()) {
249249
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');

src/commands/pasteWithBullets.ts

Lines changed: 76 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
21
import * as vscode from 'vscode';
32
import { processClipboardLinesPure, processSingleLinePaste, processTypedNodePaste } from '../utils/pasteUtils';
3+
import { ExtensionState } from '../state/extensionState';
4+
45
function detectBulletFromLine(line: string): string | null {
56
// Match a bullet at the start: bullet char + space (including '+')
67
const match = line.match(/^\s*([\u2022\-\*\•\+])\s/);
@@ -25,43 +26,90 @@ function getBulletStyle(document: vscode.TextDocument, lineNumber: number): stri
2526
}
2627

2728
export class PasteWithBullets {
29+
private extensionState: ExtensionState;
30+
31+
constructor(extensionState: ExtensionState) {
32+
this.extensionState = extensionState;
33+
}
34+
2835
public async pasteWithBulletsCommand(): Promise<void> {
2936
const editor = vscode.window.activeTextEditor;
3037
if (!editor) { return; }
3138

32-
const clipboardText = await vscode.env.clipboard.readText();
33-
const clipboardLines = clipboardText.split(/\r?\n/);
34-
if (clipboardLines.length === 0) {
39+
const documentModel = this.extensionState.getDocumentModel(editor.document.uri.toString());
40+
if (!documentModel) {
41+
// Fallback to default paste if model not found
3542
await vscode.commands.executeCommand('default:paste');
3643
return;
3744
}
3845

39-
const { selection, document } = editor;
40-
const currentLine = document.lineAt(selection.start.line);
46+
await documentModel.performBulkUpdate(async () => {
47+
const clipboardText = await vscode.env.clipboard.readText();
48+
const clipboardLines = clipboardText.split(/\r?\n/);
49+
if (clipboardLines.length === 0) {
50+
await vscode.commands.executeCommand('default:paste');
51+
return;
52+
}
4153

54+
const { selection, document } = editor;
55+
const currentLine = document.lineAt(selection.start.line);
4256

43-
// Typed node paste
44-
if (clipboardLines.length > 0 && /^\(\w+\)/.test(clipboardLines[0].trim())) {
45-
const adjustedClipboardLines = processTypedNodePaste({ clipboardLines });
46-
const textToInsert = adjustedClipboardLines.join('\n');
47-
let startLine = selection.start.line;
48-
await editor.edit(editBuilder => {
49-
editBuilder.replace(selection, textToInsert);
50-
});
51-
// Place cursor at end of last pasted line
52-
const lastLineIdx = adjustedClipboardLines.length - 1;
53-
const lastLineText = adjustedClipboardLines[lastLineIdx] || '';
54-
const newPosition = new vscode.Position(startLine + lastLineIdx, lastLineText.length);
55-
editor.selection = new vscode.Selection(newPosition, newPosition);
56-
return;
57-
}
57+
// Typed node paste
58+
if (clipboardLines.length > 0 && /^\(\w+\)/.test(clipboardLines[0].trim())) {
59+
const adjustedClipboardLines = processTypedNodePaste({ clipboardLines });
60+
const textToInsert = adjustedClipboardLines.join('\n');
61+
let startLine = selection.start.line;
62+
await editor.edit(editBuilder => {
63+
editBuilder.replace(selection, textToInsert);
64+
});
65+
// Cursor placement is tricky in bulk update; this might need adjustment
66+
const lastLineIdx = adjustedClipboardLines.length - 1;
67+
const lastLineText = adjustedClipboardLines[lastLineIdx] || '';
68+
const newPosition = new vscode.Position(startLine + lastLineIdx, lastLineText.length);
69+
editor.selection = new vscode.Selection(newPosition, newPosition);
70+
return;
71+
}
72+
73+
// Multi-line paste
74+
if (clipboardLines.length > 1) {
75+
let partBeforeCursor: string;
76+
let partAfterCursor: string;
77+
let startLine = selection.start.line;
78+
if (selection.start.character === currentLine.firstNonWhitespaceCharacterIndex) {
79+
partBeforeCursor = '';
80+
partAfterCursor = currentLine.text.substring(selection.start.character);
81+
} else {
82+
partBeforeCursor = currentLine.text.substring(0, selection.start.character);
83+
partAfterCursor = currentLine.text.substring(selection.start.character);
84+
}
85+
const bullet = getBulletStyle(document, currentLine.lineNumber);
86+
const newLines = processClipboardLinesPure({
87+
clipboardLines,
88+
partBeforeCursor,
89+
partAfterCursor,
90+
currentLineText: currentLine.text,
91+
currentLineIndent: currentLine.firstNonWhitespaceCharacterIndex,
92+
bulletTypeForLine: () => ({ bulletType: 'none' }),
93+
getBullet: () => bullet,
94+
document,
95+
lineNumber: currentLine.lineNumber
96+
});
97+
await editor.edit(editBuilder => {
98+
editBuilder.replace(currentLine.range, newLines.join('\n'));
99+
});
100+
const lastIdx = newLines.length - 1;
101+
const lastLineText = newLines[lastIdx];
102+
const afterTextLen = partAfterCursor.length;
103+
const newChar = Math.max(0, lastLineText.length - afterTextLen);
104+
const newPosition = new vscode.Position(startLine + lastIdx, newChar);
105+
editor.selection = new vscode.Selection(newPosition, newPosition);
106+
return;
107+
}
58108

59-
// Multi-line paste
60-
if (clipboardLines.length > 1) {
109+
// Single-line paste
61110
let partBeforeCursor: string;
62111
let partAfterCursor: string;
63112
let startLine = selection.start.line;
64-
// If cursor is at first non-whitespace character, treat as start of line for bullet logic
65113
if (selection.start.character === currentLine.firstNonWhitespaceCharacterIndex) {
66114
partBeforeCursor = '';
67115
partAfterCursor = currentLine.text.substring(selection.start.character);
@@ -71,7 +119,7 @@ export class PasteWithBullets {
71119
}
72120
const bullet = getBulletStyle(document, currentLine.lineNumber);
73121
const newLines = processClipboardLinesPure({
74-
clipboardLines,
122+
clipboardLines: [clipboardLines[0]],
75123
partBeforeCursor,
76124
partAfterCursor,
77125
currentLineText: currentLine.text,
@@ -82,48 +130,12 @@ export class PasteWithBullets {
82130
lineNumber: currentLine.lineNumber
83131
});
84132
await editor.edit(editBuilder => {
85-
editBuilder.replace(currentLine.range, newLines.join('\n'));
133+
editBuilder.replace(currentLine.range, newLines[0]);
86134
});
87-
// Place cursor just before the original partAfterCursor on the last pasted line
88-
const lastIdx = newLines.length - 1;
89-
const lastLineText = newLines[lastIdx];
90135
const afterTextLen = partAfterCursor.length;
91-
const newChar = Math.max(0, lastLineText.length - afterTextLen);
92-
const newPosition = new vscode.Position(startLine + lastIdx, newChar);
136+
const newChar = Math.max(0, newLines[0].length - afterTextLen);
137+
const newPosition = new vscode.Position(startLine, newChar);
93138
editor.selection = new vscode.Selection(newPosition, newPosition);
94-
return;
95-
}
96-
97-
// Single-line paste: use the same bullet detection and context as multi-line paste
98-
let partBeforeCursor: string;
99-
let partAfterCursor: string;
100-
let startLine = selection.start.line;
101-
if (selection.start.character === currentLine.firstNonWhitespaceCharacterIndex) {
102-
partBeforeCursor = '';
103-
partAfterCursor = currentLine.text.substring(selection.start.character);
104-
} else {
105-
partBeforeCursor = currentLine.text.substring(0, selection.start.character);
106-
partAfterCursor = currentLine.text.substring(selection.start.character);
107-
}
108-
const bullet = getBulletStyle(document, currentLine.lineNumber);
109-
const newLines = processClipboardLinesPure({
110-
clipboardLines: [clipboardLines[0]],
111-
partBeforeCursor,
112-
partAfterCursor,
113-
currentLineText: currentLine.text,
114-
currentLineIndent: currentLine.firstNonWhitespaceCharacterIndex,
115-
bulletTypeForLine: () => ({ bulletType: 'none' }),
116-
getBullet: () => bullet,
117-
document,
118-
lineNumber: currentLine.lineNumber
119-
});
120-
await editor.edit(editBuilder => {
121-
editBuilder.replace(currentLine.range, newLines[0]);
122139
});
123-
// Place cursor just before the original partAfterCursor on the pasted line
124-
const afterTextLen = partAfterCursor.length;
125-
const newChar = Math.max(0, newLines[0].length - afterTextLen);
126-
const newPosition = new vscode.Position(startLine, newChar);
127-
editor.selection = new vscode.Selection(newPosition, newPosition);
128140
}
129141
}

src/decorations/decorationCalculator.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as vscode from 'vscode';
22
import { BlockNode } from '../document/blockNode';
3-
import { withTiming } from '../utils/debugUtils';
43

54
/**
65
* A stateless calculator responsible for determining which decorations to apply based on the properties of `BlockNode`s.
@@ -16,34 +15,32 @@ export class DecorationCalculator {
1615
* and values are arrays of `DecorationOptions` to be applied.
1716
*/
1817
public static calculateDecorations(nodes: BlockNode[], decorationsMap: Map<string, vscode.DecorationOptions[]>): void {
19-
withTiming(() => {
20-
for (const node of nodes) {
21-
// Skip decoration for code blocks, markdown headers, or empty lines.
22-
if (node.isCodeBlockDelimiter || node.isExcluded || node.line.isEmptyOrWhitespace) {
23-
continue;
24-
}
18+
for (const node of nodes) {
19+
// Skip decoration for code blocks, markdown headers, or empty lines.
20+
if (node.isCodeBlockDelimiter || node.isExcluded || node.line.isEmptyOrWhitespace) {
21+
continue;
22+
}
2523

26-
// Handle typed nodes like `(Book)`.
27-
if (node.isTypedNode && node.typedNodeRange) {
28-
decorationsMap.get('typedNodeDecorationType')!.push({ range: node.typedNodeRange });
29-
continue; // Typed nodes don't have other decorations.
30-
}
24+
// Handle typed nodes like `(Book)`.
25+
if (node.isTypedNode && node.typedNodeRange) {
26+
decorationsMap.get('typedNodeDecorationType')!.push({ range: node.typedNodeRange });
27+
continue; // Typed nodes don't have other decorations.
28+
}
3129

32-
// Handle key-value pairs like `Author:: John Doe`.
33-
if (node.isKeyValue && node.keyValue) {
34-
decorationsMap.get('keyValueDecorationType')!.push({ range: node.keyValue.keyRange });
35-
continue; // Key-value pairs don't have other decorations.
36-
}
30+
// Handle key-value pairs like `Author:: John Doe`.
31+
if (node.isKeyValue && node.keyValue) {
32+
decorationsMap.get('keyValueDecorationType')!.push({ range: node.keyValue.keyRange });
33+
continue; // Key-value pairs don't have other decorations.
34+
}
3735

38-
// Handle various bullet point types.
39-
if (node.bulletRange) {
40-
const decorationType = DecorationCalculator.getDecorationTypeForBullet(node.bulletType); // Corrected 'this' to 'DecorationCalculator'
41-
if (decorationType) {
42-
decorationsMap.get(decorationType)!.push({ range: node.bulletRange });
43-
}
36+
// Handle various bullet point types.
37+
if (node.bulletRange) {
38+
const decorationType = DecorationCalculator.getDecorationTypeForBullet(node.bulletType); // Corrected 'this' to 'DecorationCalculator'
39+
if (decorationType) {
40+
decorationsMap.get(decorationType)!.push({ range: node.bulletRange });
4441
}
4542
}
46-
}, `Decoration calculation for ${nodes.length} nodes`);
43+
}
4744
}
4845

4946
/**

src/decorations/decorationManager.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { DecorationCalculator } from './decorationCalculator';
55
import { debounce } from '../utils/debounce';
66
import { Configuration } from '../config/configuration';
77
import { ExtensionState } from '../state/extensionState';
8-
import { withTiming } from '../utils/debugUtils';
98

109
/**
1110
* Manages the application of text editor decorations. It orchestrates decoration updates
@@ -15,7 +14,7 @@ import { withTiming } from '../utils/debugUtils';
1514
export class DecorationManager implements vscode.Disposable {
1615
private _decorationTypes: Map<string, vscode.TextEditorDecorationType> = new Map();
1716
private _disposables: vscode.Disposable[] = [];
18-
private _debouncedUpdate: (editor: vscode.TextEditor, tree: DocumentTree) => void;
17+
private _debouncedUpdate: (editor: vscode.TextEditor, tree: DocumentTree) => void;
1918
private _extensionState: ExtensionState;
2019

2120
constructor(extensionState: ExtensionState) {
@@ -93,17 +92,15 @@ private _debouncedUpdate: (editor: vscode.TextEditor, tree: DocumentTree) => voi
9392
* @param tree The current `DocumentTree`.
9493
*/
9594
private applyDecorations(editor: vscode.TextEditor, tree: DocumentTree): void {
96-
withTiming(() => {
97-
const decorationsToApply = new Map<string, vscode.DecorationOptions[]>();
98-
this._decorationTypes.forEach((_, key) => decorationsToApply.set(key, []));
95+
const decorationsToApply = new Map<string, vscode.DecorationOptions[]>();
96+
this._decorationTypes.forEach((_, key) => decorationsToApply.set(key, []));
9997

10098
const nodesToDecorate = this.getNodesInViewport(editor, tree);
10199
DecorationCalculator.calculateDecorations(nodesToDecorate, decorationsToApply);
102100

103-
this._decorationTypes.forEach((decorationType, typeName) => {
104-
editor.setDecorations(decorationType, decorationsToApply.get(typeName) || []);
105-
});
106-
}, `Decoration rendering for ${editor.document.uri.fsPath}`);
101+
this._decorationTypes.forEach((decorationType, typeName) => {
102+
editor.setDecorations(decorationType, decorationsToApply.get(typeName) || []);
103+
});
107104
}
108105

109106
/**

0 commit comments

Comments
 (0)