Skip to content

Commit dea209e

Browse files
committed
Move DocumentContentChangeAction into its own file
1 parent 9d06931 commit dea209e

File tree

4 files changed

+167
-162
lines changed

4 files changed

+167
-162
lines changed

src/actions/commands/actions.ts

Lines changed: 0 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -33,170 +33,10 @@ import {
3333
} from './../../mode/mode';
3434
import { Register, RegisterMode } from './../../register/register';
3535
import { TextEditor } from './../../textEditor';
36-
import { Transformation } from './../../transformations/transformations';
3736
import { BaseCommand, RegisterAction } from './../base';
3837
import * as operator from './../operator';
3938
import { Uri } from 'vscode';
4039

41-
/**
42-
* A very special snowflake.
43-
*
44-
* Each keystroke when typing in Insert mode is its own Action, which means naively replaying a
45-
* realistic insertion (via `.` or a macro) does many small insertions, which is very slow.
46-
* So instead, we fold all those actions after the fact into a single DocumentContentChangeAction,
47-
* which compresses the changes, generally into a single document edit per cursor.
48-
*/
49-
export class DocumentContentChangeAction extends BaseCommand {
50-
modes = [];
51-
keys = [];
52-
private readonly cursorStart: Position;
53-
private cursorEnd: Position;
54-
55-
constructor(cursorStart: Position) {
56-
super();
57-
this.cursorStart = cursorStart;
58-
this.cursorEnd = cursorStart;
59-
}
60-
61-
private contentChanges: vscode.TextDocumentContentChangeEvent[] = [];
62-
63-
public addChanges(changes: vscode.TextDocumentContentChangeEvent[], cursorPosition: Position) {
64-
this.contentChanges = [...this.contentChanges, ...changes];
65-
this.compressChanges();
66-
this.cursorEnd = cursorPosition;
67-
}
68-
69-
public getTransformation(positionDiff: PositionDiff): Transformation {
70-
return {
71-
type: 'contentChange',
72-
changes: this.contentChanges,
73-
diff: positionDiff,
74-
};
75-
}
76-
77-
public override async exec(position: Position, vimState: VimState): Promise<void> {
78-
if (this.contentChanges.length === 0) {
79-
return;
80-
}
81-
82-
let originalLeftBoundary = this.cursorStart;
83-
84-
let rightBoundary: Position = position;
85-
for (const change of this.contentChanges) {
86-
if (change.range.start.line < originalLeftBoundary.line) {
87-
// This change should be ignored
88-
const linesAffected = change.range.end.line - change.range.start.line + 1;
89-
const resultLines = change.text.split('\n').length;
90-
originalLeftBoundary = originalLeftBoundary.with(
91-
Math.max(0, originalLeftBoundary.line + resultLines - linesAffected),
92-
);
93-
continue;
94-
}
95-
96-
// Translates diffPos from a position relative to originalLeftBoundary to one relative to position
97-
const translate = (diffPos: Position): Position => {
98-
const lineOffset = diffPos.line - originalLeftBoundary.line;
99-
const char =
100-
lineOffset === 0
101-
? position.character + diffPos.character - originalLeftBoundary.character
102-
: diffPos.character;
103-
// TODO: Should we document.validate() this position?
104-
return new Position(Math.max(position.line + lineOffset, 0), Math.max(char, 0));
105-
};
106-
107-
const replaceRange = new vscode.Range(
108-
translate(change.range.start),
109-
translate(change.range.end),
110-
);
111-
112-
if (replaceRange.start.isAfter(rightBoundary)) {
113-
// This change should be ignored as it's out of boundary
114-
continue;
115-
}
116-
117-
// Calculate new right boundary
118-
const textDiffLines = change.text.split('\n');
119-
const numLinesAdded = textDiffLines.length - 1;
120-
const newRightBoundary =
121-
numLinesAdded === 0
122-
? new Position(replaceRange.start.line, replaceRange.start.character + change.text.length)
123-
: new Position(replaceRange.start.line + numLinesAdded, textDiffLines.pop()!.length);
124-
125-
rightBoundary = laterOf(rightBoundary, newRightBoundary);
126-
127-
if (replaceRange.start.isEqual(replaceRange.end)) {
128-
vimState.recordedState.transformer.insert(
129-
replaceRange.start,
130-
change.text,
131-
PositionDiff.exactPosition(translate(this.cursorEnd)),
132-
);
133-
} else {
134-
vimState.recordedState.transformer.replace(
135-
replaceRange,
136-
change.text,
137-
PositionDiff.exactPosition(translate(this.cursorEnd)),
138-
);
139-
}
140-
}
141-
}
142-
143-
private compressChanges(): void {
144-
const merge = (
145-
first: vscode.TextDocumentContentChangeEvent,
146-
second: vscode.TextDocumentContentChangeEvent,
147-
): vscode.TextDocumentContentChangeEvent | undefined => {
148-
if (first.rangeOffset + first.text.length === second.rangeOffset) {
149-
// Simple concatenation
150-
return {
151-
text: first.text + second.text,
152-
range: first.range,
153-
rangeOffset: first.rangeOffset,
154-
rangeLength: first.rangeLength,
155-
};
156-
} else if (
157-
first.rangeOffset <= second.rangeOffset &&
158-
first.text.length >= second.rangeLength
159-
) {
160-
const start = second.rangeOffset - first.rangeOffset;
161-
const end = start + second.rangeLength;
162-
const text = first.text.slice(0, start) + second.text + first.text.slice(end);
163-
// `second` replaces part of `first`
164-
// Most often, this is the result of confirming an auto-completion
165-
return {
166-
text,
167-
range: first.range,
168-
rangeOffset: first.rangeOffset,
169-
rangeLength: first.rangeLength,
170-
};
171-
} else {
172-
// TODO: Do any of the cases falling into this `else` matter?
173-
// TODO: YES - make an insertion and then autocomplete to something totally different (replace subsumes insert)
174-
return undefined;
175-
}
176-
};
177-
178-
const compressed: vscode.TextDocumentContentChangeEvent[] = [];
179-
let prev: vscode.TextDocumentContentChangeEvent | undefined;
180-
for (const change of this.contentChanges) {
181-
if (prev === undefined) {
182-
prev = change;
183-
} else {
184-
const merged = merge(prev, change);
185-
if (merged) {
186-
prev = merged;
187-
} else {
188-
compressed.push(prev);
189-
prev = change;
190-
}
191-
}
192-
}
193-
if (prev !== undefined) {
194-
compressed.push(prev);
195-
}
196-
this.contentChanges = compressed;
197-
}
198-
}
199-
20040
@RegisterAction
20141
class DisableExtension extends BaseCommand {
20242
modes = [
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import * as vscode from 'vscode';
2+
import { Position } from 'vscode';
3+
import { PositionDiff, laterOf } from '../../common/motion/position';
4+
import { VimState } from '../../state/vimState';
5+
import { Transformation } from '../../transformations/transformations';
6+
import { BaseCommand } from '../base';
7+
8+
/**
9+
* A very special snowflake.
10+
*
11+
* Each keystroke when typing in Insert mode is its own Action, which means naively replaying a
12+
* realistic insertion (via `.` or a macro) does many small insertions, which is very slow.
13+
* So instead, we fold all those actions after the fact into a single DocumentContentChangeAction,
14+
* which compresses the changes, generally into a single document edit per cursor.
15+
*/
16+
export class DocumentContentChangeAction extends BaseCommand {
17+
modes = [];
18+
keys = [];
19+
private readonly cursorStart: Position;
20+
private cursorEnd: Position;
21+
22+
constructor(cursorStart: Position) {
23+
super();
24+
this.cursorStart = cursorStart;
25+
this.cursorEnd = cursorStart;
26+
}
27+
28+
private contentChanges: vscode.TextDocumentContentChangeEvent[] = [];
29+
30+
public addChanges(changes: vscode.TextDocumentContentChangeEvent[], cursorPosition: Position) {
31+
this.contentChanges = [...this.contentChanges, ...changes];
32+
this.compressChanges();
33+
this.cursorEnd = cursorPosition;
34+
}
35+
36+
public getTransformation(positionDiff: PositionDiff): Transformation {
37+
return {
38+
type: 'contentChange',
39+
changes: this.contentChanges,
40+
diff: positionDiff,
41+
};
42+
}
43+
44+
public override async exec(position: Position, vimState: VimState): Promise<void> {
45+
if (this.contentChanges.length === 0) {
46+
return;
47+
}
48+
49+
let originalLeftBoundary = this.cursorStart;
50+
51+
let rightBoundary: Position = position;
52+
for (const change of this.contentChanges) {
53+
if (change.range.start.line < originalLeftBoundary.line) {
54+
// This change should be ignored
55+
const linesAffected = change.range.end.line - change.range.start.line + 1;
56+
const resultLines = change.text.split('\n').length;
57+
originalLeftBoundary = originalLeftBoundary.with(
58+
Math.max(0, originalLeftBoundary.line + resultLines - linesAffected),
59+
);
60+
continue;
61+
}
62+
63+
// Translates diffPos from a position relative to originalLeftBoundary to one relative to position
64+
const translate = (diffPos: Position): Position => {
65+
const lineOffset = diffPos.line - originalLeftBoundary.line;
66+
const char =
67+
lineOffset === 0
68+
? position.character + diffPos.character - originalLeftBoundary.character
69+
: diffPos.character;
70+
// TODO: Should we document.validate() this position?
71+
return new Position(Math.max(position.line + lineOffset, 0), Math.max(char, 0));
72+
};
73+
74+
const replaceRange = new vscode.Range(
75+
translate(change.range.start),
76+
translate(change.range.end),
77+
);
78+
79+
if (replaceRange.start.isAfter(rightBoundary)) {
80+
// This change should be ignored as it's out of boundary
81+
continue;
82+
}
83+
84+
// Calculate new right boundary
85+
const textDiffLines = change.text.split('\n');
86+
const numLinesAdded = textDiffLines.length - 1;
87+
const newRightBoundary =
88+
numLinesAdded === 0
89+
? new Position(replaceRange.start.line, replaceRange.start.character + change.text.length)
90+
: new Position(replaceRange.start.line + numLinesAdded, textDiffLines.pop()!.length);
91+
92+
rightBoundary = laterOf(rightBoundary, newRightBoundary);
93+
94+
if (replaceRange.start.isEqual(replaceRange.end)) {
95+
vimState.recordedState.transformer.insert(
96+
replaceRange.start,
97+
change.text,
98+
PositionDiff.exactPosition(translate(this.cursorEnd)),
99+
);
100+
} else {
101+
vimState.recordedState.transformer.replace(
102+
replaceRange,
103+
change.text,
104+
PositionDiff.exactPosition(translate(this.cursorEnd)),
105+
);
106+
}
107+
}
108+
}
109+
110+
private compressChanges(): void {
111+
const merge = (
112+
first: vscode.TextDocumentContentChangeEvent,
113+
second: vscode.TextDocumentContentChangeEvent,
114+
): vscode.TextDocumentContentChangeEvent | undefined => {
115+
if (first.rangeOffset + first.text.length === second.rangeOffset) {
116+
// Simple concatenation
117+
return {
118+
text: first.text + second.text,
119+
range: first.range,
120+
rangeOffset: first.rangeOffset,
121+
rangeLength: first.rangeLength,
122+
};
123+
} else if (
124+
first.rangeOffset <= second.rangeOffset &&
125+
first.text.length >= second.rangeLength
126+
) {
127+
const start = second.rangeOffset - first.rangeOffset;
128+
const end = start + second.rangeLength;
129+
const text = first.text.slice(0, start) + second.text + first.text.slice(end);
130+
// `second` replaces part of `first`
131+
// Most often, this is the result of confirming an auto-completion
132+
return {
133+
text,
134+
range: first.range,
135+
rangeOffset: first.rangeOffset,
136+
rangeLength: first.rangeLength,
137+
};
138+
} else {
139+
// TODO: Do any of the cases falling into this `else` matter?
140+
// TODO: YES - make an insertion and then autocomplete to something totally different (replace subsumes insert)
141+
return undefined;
142+
}
143+
};
144+
145+
const compressed: vscode.TextDocumentContentChangeEvent[] = [];
146+
let prev: vscode.TextDocumentContentChangeEvent | undefined;
147+
for (const change of this.contentChanges) {
148+
if (prev === undefined) {
149+
prev = change;
150+
} else {
151+
const merged = merge(prev, change);
152+
if (merged) {
153+
prev = merged;
154+
} else {
155+
compressed.push(prev);
156+
prev = change;
157+
}
158+
}
159+
}
160+
if (prev !== undefined) {
161+
compressed.push(prev);
162+
}
163+
this.contentChanges = compressed;
164+
}
165+
}

src/actions/commands/insert.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import {
2424
CommandInsertNewLineAbove,
2525
CommandInsertNewLineBefore,
2626
CommandReplaceAtCursorFromNormalMode,
27-
DocumentContentChangeAction,
2827
} from './actions';
28+
import { DocumentContentChangeAction } from './documentChange';
2929
import { DefaultDigraphs } from './digraphs';
3030

3131
@RegisterAction

src/mode/modeHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import {
3333
CommandNumber,
3434
CommandQuitRecordMacro,
3535
CommandRegister,
36-
DocumentContentChangeAction,
3736
} from './../actions/commands/actions';
3837
import {
3938
CommandBackspaceInInsertMode,
@@ -60,6 +59,7 @@ import {
6059
isStatusBarMode,
6160
isVisualMode,
6261
} from './mode';
62+
import { DocumentContentChangeAction } from '../actions/commands/documentChange';
6363

6464
interface IModeHandlerMap {
6565
get(editorId: Uri): ModeHandler | undefined;

0 commit comments

Comments
 (0)