@pierre/diffs 1.2.12 → 1.3.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CodeView.d.ts +91 -6
- package/dist/components/CodeView.d.ts.map +1 -1
- package/dist/components/CodeView.js +300 -38
- package/dist/components/CodeView.js.map +1 -1
- package/dist/components/File.d.ts +14 -3
- package/dist/components/File.d.ts.map +1 -1
- package/dist/components/File.js +98 -12
- package/dist/components/File.js.map +1 -1
- package/dist/components/FileDiff.d.ts +68 -19
- package/dist/components/FileDiff.d.ts.map +1 -1
- package/dist/components/FileDiff.js +363 -25
- package/dist/components/FileDiff.js.map +1 -1
- package/dist/components/FileStream.d.ts.map +1 -1
- package/dist/components/FileStream.js +2 -1
- package/dist/components/FileStream.js.map +1 -1
- package/dist/components/UnresolvedFile.d.ts +2 -2
- package/dist/components/UnresolvedFile.d.ts.map +1 -1
- package/dist/components/UnresolvedFile.js +2 -2
- package/dist/components/UnresolvedFile.js.map +1 -1
- package/dist/components/VirtualizedFile.d.ts +4 -2
- package/dist/components/VirtualizedFile.d.ts.map +1 -1
- package/dist/components/VirtualizedFile.js +59 -47
- package/dist/components/VirtualizedFile.js.map +1 -1
- package/dist/components/VirtualizedFileDiff.d.ts +27 -5
- package/dist/components/VirtualizedFileDiff.d.ts.map +1 -1
- package/dist/components/VirtualizedFileDiff.js +262 -56
- package/dist/components/VirtualizedFileDiff.js.map +1 -1
- package/dist/components/Virtualizer.d.ts +4 -1
- package/dist/components/Virtualizer.d.ts.map +1 -1
- package/dist/components/Virtualizer.js +21 -4
- package/dist/components/Virtualizer.js.map +1 -1
- package/dist/constants.d.ts +4 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -1
- package/dist/constants.js.map +1 -1
- package/dist/editor/command.d.ts +7 -0
- package/dist/editor/command.d.ts.map +1 -0
- package/dist/editor/command.js +54 -0
- package/dist/editor/command.js.map +1 -0
- package/dist/editor/editStack.d.ts +71 -0
- package/dist/editor/editStack.d.ts.map +1 -0
- package/dist/editor/editStack.js +218 -0
- package/dist/editor/editStack.js.map +1 -0
- package/dist/editor/editor.d.ts +105 -0
- package/dist/editor/editor.d.ts.map +1 -0
- package/dist/editor/editor.js +2545 -0
- package/dist/editor/editor.js.map +1 -0
- package/dist/editor/editor2.js +6 -0
- package/dist/editor/editor2.js.map +1 -0
- package/dist/editor/index.d.ts +5 -0
- package/dist/editor/index.js +3 -0
- package/dist/editor/languages.d.ts +26 -0
- package/dist/editor/languages.d.ts.map +1 -0
- package/dist/editor/languages.js +363 -0
- package/dist/editor/languages.js.map +1 -0
- package/dist/editor/lineAnnotations.d.ts +9 -0
- package/dist/editor/lineAnnotations.d.ts.map +1 -0
- package/dist/editor/lineAnnotations.js +212 -0
- package/dist/editor/lineAnnotations.js.map +1 -0
- package/dist/editor/marker.d.ts +38 -0
- package/dist/editor/marker.d.ts.map +1 -0
- package/dist/editor/marker.js +245 -0
- package/dist/editor/marker.js.map +1 -0
- package/dist/editor/matchBrackets.d.ts +9 -0
- package/dist/editor/matchBrackets.d.ts.map +1 -0
- package/dist/editor/matchBrackets.js +138 -0
- package/dist/editor/matchBrackets.js.map +1 -0
- package/dist/editor/pieceTable.d.ts +41 -0
- package/dist/editor/pieceTable.d.ts.map +1 -0
- package/dist/editor/pieceTable.js +671 -0
- package/dist/editor/pieceTable.js.map +1 -0
- package/dist/editor/platform.d.ts +19 -0
- package/dist/editor/platform.d.ts.map +1 -0
- package/dist/editor/platform.js +57 -0
- package/dist/editor/platform.js.map +1 -0
- package/dist/editor/popover.d.ts +65 -0
- package/dist/editor/popover.d.ts.map +1 -0
- package/dist/editor/popover.js +187 -0
- package/dist/editor/popover.js.map +1 -0
- package/dist/editor/searchPanel.d.ts +40 -0
- package/dist/editor/searchPanel.d.ts.map +1 -0
- package/dist/editor/searchPanel.js +318 -0
- package/dist/editor/searchPanel.js.map +1 -0
- package/dist/editor/selection.d.ts +211 -0
- package/dist/editor/selection.d.ts.map +1 -0
- package/dist/editor/selection.js +1626 -0
- package/dist/editor/selection.js.map +1 -0
- package/dist/editor/selectionAction.d.ts +46 -0
- package/dist/editor/selectionAction.d.ts.map +1 -0
- package/dist/editor/selectionAction.js +64 -0
- package/dist/editor/selectionAction.js.map +1 -0
- package/dist/editor/sprite.d.ts +8 -0
- package/dist/editor/sprite.d.ts.map +1 -0
- package/dist/editor/sprite.js +45 -0
- package/dist/editor/sprite.js.map +1 -0
- package/dist/editor/stateStorage.d.ts +15 -0
- package/dist/editor/stateStorage.d.ts.map +1 -0
- package/dist/editor/stateStorage.js +65 -0
- package/dist/editor/stateStorage.js.map +1 -0
- package/dist/editor/textDocument.d.ts +77 -0
- package/dist/editor/textDocument.d.ts.map +1 -0
- package/dist/editor/textDocument.js +271 -0
- package/dist/editor/textDocument.js.map +1 -0
- package/dist/editor/textMeasure.d.ts +61 -0
- package/dist/editor/textMeasure.d.ts.map +1 -0
- package/dist/editor/textMeasure.js +209 -0
- package/dist/editor/textMeasure.js.map +1 -0
- package/dist/editor/tokenizer.d.ts +41 -0
- package/dist/editor/tokenizer.d.ts.map +1 -0
- package/dist/editor/tokenizer.js +513 -0
- package/dist/editor/tokenizer.js.map +1 -0
- package/dist/editor/utils.d.ts +21 -0
- package/dist/editor/utils.d.ts.map +1 -0
- package/dist/editor/utils.js +68 -0
- package/dist/editor/utils.js.map +1 -0
- package/dist/index.d.ts +10 -8
- package/dist/index.js +12 -10
- package/dist/managers/InteractionManager.d.ts +7 -0
- package/dist/managers/InteractionManager.d.ts.map +1 -1
- package/dist/managers/InteractionManager.js +22 -1
- package/dist/managers/InteractionManager.js.map +1 -1
- package/dist/managers/ResizeManager.d.ts +10 -2
- package/dist/managers/ResizeManager.d.ts.map +1 -1
- package/dist/managers/ResizeManager.js +33 -13
- package/dist/managers/ResizeManager.js.map +1 -1
- package/dist/managers/UniversalRenderingManager.d.ts +2 -1
- package/dist/managers/UniversalRenderingManager.d.ts.map +1 -1
- package/dist/managers/UniversalRenderingManager.js +6 -1
- package/dist/managers/UniversalRenderingManager.js.map +1 -1
- package/dist/react/CodeView.d.ts +26 -1
- package/dist/react/CodeView.d.ts.map +1 -1
- package/dist/react/CodeView.js +82 -19
- package/dist/react/CodeView.js.map +1 -1
- package/dist/react/EditContext.d.ts +15 -0
- package/dist/react/EditContext.d.ts.map +1 -0
- package/dist/react/EditContext.js +23 -0
- package/dist/react/EditContext.js.map +1 -0
- package/dist/react/File.d.ts +3 -1
- package/dist/react/File.d.ts.map +1 -1
- package/dist/react/File.js +4 -2
- package/dist/react/File.js.map +1 -1
- package/dist/react/FileDiff.d.ts +4 -1
- package/dist/react/FileDiff.d.ts.map +1 -1
- package/dist/react/FileDiff.js +4 -2
- package/dist/react/FileDiff.js.map +1 -1
- package/dist/react/MultiFileDiff.d.ts +7 -5
- package/dist/react/MultiFileDiff.d.ts.map +1 -1
- package/dist/react/MultiFileDiff.js +4 -2
- package/dist/react/MultiFileDiff.js.map +1 -1
- package/dist/react/PatchDiff.d.ts +4 -1
- package/dist/react/PatchDiff.d.ts.map +1 -1
- package/dist/react/PatchDiff.js +4 -2
- package/dist/react/PatchDiff.js.map +1 -1
- package/dist/react/UnresolvedFile.d.ts +1 -0
- package/dist/react/UnresolvedFile.d.ts.map +1 -1
- package/dist/react/UnresolvedFile.js +2 -1
- package/dist/react/UnresolvedFile.js.map +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +2 -1
- package/dist/react/types.d.ts +3 -0
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/utils/renderDiffChildren.d.ts +2 -0
- package/dist/react/utils/renderDiffChildren.d.ts.map +1 -1
- package/dist/react/utils/renderDiffChildren.js +16 -8
- package/dist/react/utils/renderDiffChildren.js.map +1 -1
- package/dist/react/utils/renderFileChildren.d.ts +2 -0
- package/dist/react/utils/renderFileChildren.d.ts.map +1 -1
- package/dist/react/utils/renderFileChildren.js +16 -8
- package/dist/react/utils/renderFileChildren.js.map +1 -1
- package/dist/react/utils/useFileDiffInstance.d.ts +3 -1
- package/dist/react/utils/useFileDiffInstance.d.ts.map +1 -1
- package/dist/react/utils/useFileDiffInstance.js +30 -6
- package/dist/react/utils/useFileDiffInstance.js.map +1 -1
- package/dist/react/utils/useFileInstance.d.ts +4 -1
- package/dist/react/utils/useFileInstance.d.ts.map +1 -1
- package/dist/react/utils/useFileInstance.js +30 -5
- package/dist/react/utils/useFileInstance.js.map +1 -1
- package/dist/renderers/DiffHunksRenderer.d.ts +28 -2
- package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
- package/dist/renderers/DiffHunksRenderer.js +244 -22
- package/dist/renderers/DiffHunksRenderer.js.map +1 -1
- package/dist/renderers/FileRenderer.d.ts +8 -1
- package/dist/renderers/FileRenderer.d.ts.map +1 -1
- package/dist/renderers/FileRenderer.js +155 -45
- package/dist/renderers/FileRenderer.js.map +1 -1
- package/dist/ssr/index.d.ts +2 -2
- package/dist/ssr/preloadDiffs.d.ts +11 -10
- package/dist/ssr/preloadDiffs.d.ts.map +1 -1
- package/dist/ssr/preloadDiffs.js +15 -7
- package/dist/ssr/preloadDiffs.js.map +1 -1
- package/dist/style.js +1 -1
- package/dist/types.d.ts +282 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/applyLineTextWithNewline.d.ts +5 -0
- package/dist/utils/applyLineTextWithNewline.d.ts.map +1 -0
- package/dist/utils/applyLineTextWithNewline.js +11 -0
- package/dist/utils/applyLineTextWithNewline.js.map +1 -0
- package/dist/utils/areHunkDataEqual.js +1 -1
- package/dist/utils/areHunkDataEqual.js.map +1 -1
- package/dist/utils/areManagedSnapshotsEqual.d.ts +2 -2
- package/dist/utils/areManagedSnapshotsEqual.d.ts.map +1 -1
- package/dist/utils/areManagedSnapshotsEqual.js +5 -0
- package/dist/utils/areManagedSnapshotsEqual.js.map +1 -1
- package/dist/utils/awaitWithTimeout.d.ts +5 -0
- package/dist/utils/awaitWithTimeout.d.ts.map +1 -0
- package/dist/utils/awaitWithTimeout.js +15 -0
- package/dist/utils/awaitWithTimeout.js.map +1 -0
- package/dist/utils/cleanLastNewline.js +6 -1
- package/dist/utils/cleanLastNewline.js.map +1 -1
- package/dist/utils/cloneFileDiffMetadata.d.ts +7 -0
- package/dist/utils/cloneFileDiffMetadata.d.ts.map +1 -0
- package/dist/utils/cloneFileDiffMetadata.js +16 -0
- package/dist/utils/cloneFileDiffMetadata.js.map +1 -0
- package/dist/utils/computeEstimatedDiffHeights.d.ts +3 -1
- package/dist/utils/computeEstimatedDiffHeights.d.ts.map +1 -1
- package/dist/utils/computeEstimatedDiffHeights.js +8 -1
- package/dist/utils/computeEstimatedDiffHeights.js.map +1 -1
- package/dist/utils/computeFileOffsets.d.ts +21 -0
- package/dist/utils/computeFileOffsets.d.ts.map +1 -0
- package/dist/utils/computeFileOffsets.js +51 -0
- package/dist/utils/computeFileOffsets.js.map +1 -0
- package/dist/utils/createCodeViewHeaderFooterHostElement.d.ts +5 -0
- package/dist/utils/createCodeViewHeaderFooterHostElement.d.ts.map +1 -0
- package/dist/utils/createCodeViewHeaderFooterHostElement.js +19 -0
- package/dist/utils/createCodeViewHeaderFooterHostElement.js.map +1 -0
- package/dist/utils/createFileHeaderElement.d.ts.map +1 -1
- package/dist/utils/createFileHeaderElement.js +5 -1
- package/dist/utils/createFileHeaderElement.js.map +1 -1
- package/dist/utils/createTransformerWithState.js +9 -0
- package/dist/utils/createTransformerWithState.js.map +1 -1
- package/dist/utils/editSessionHunks.d.ts +84 -0
- package/dist/utils/editSessionHunks.d.ts.map +1 -0
- package/dist/utils/editSessionHunks.js +366 -0
- package/dist/utils/editSessionHunks.js.map +1 -0
- package/dist/utils/getDiffFileInput.d.ts +14 -0
- package/dist/utils/getDiffFileInput.d.ts.map +1 -0
- package/dist/utils/getDiffFileInput.js +24 -0
- package/dist/utils/getDiffFileInput.js.map +1 -0
- package/dist/utils/getDiffHunksRendererOptions.js +1 -0
- package/dist/utils/getDiffHunksRendererOptions.js.map +1 -1
- package/dist/utils/getFiletypeFromFileName.d.ts.map +1 -1
- package/dist/utils/getFiletypeFromFileName.js +2 -0
- package/dist/utils/getFiletypeFromFileName.js.map +1 -1
- package/dist/utils/hydratePartialDiff.d.ts +10 -0
- package/dist/utils/hydratePartialDiff.d.ts.map +1 -0
- package/dist/utils/hydratePartialDiff.js +140 -0
- package/dist/utils/hydratePartialDiff.js.map +1 -0
- package/dist/utils/iterateOverDiff.js +3 -3
- package/dist/utils/iterateOverDiff.js.map +1 -1
- package/dist/utils/parseDiffFromFile.d.ts +1 -1
- package/dist/utils/parseDiffFromFile.d.ts.map +1 -1
- package/dist/utils/parseDiffFromFile.js +26 -5
- package/dist/utils/parseDiffFromFile.js.map +1 -1
- package/dist/utils/parsePatchFiles.d.ts.map +1 -1
- package/dist/utils/parsePatchFiles.js +2 -0
- package/dist/utils/parsePatchFiles.js.map +1 -1
- package/dist/utils/realignChangeContent.d.ts +28 -0
- package/dist/utils/realignChangeContent.d.ts.map +1 -0
- package/dist/utils/realignChangeContent.js +142 -0
- package/dist/utils/realignChangeContent.js.map +1 -0
- package/dist/utils/renderDiffWithHighlighter.js +1 -1
- package/dist/utils/renderFileWithHighlighter.d.ts.map +1 -1
- package/dist/utils/renderFileWithHighlighter.js +5 -14
- package/dist/utils/renderFileWithHighlighter.js.map +1 -1
- package/dist/utils/updateDiffHunks.d.ts +24 -0
- package/dist/utils/updateDiffHunks.d.ts.map +1 -0
- package/dist/utils/updateDiffHunks.js +235 -0
- package/dist/utils/updateDiffHunks.js.map +1 -0
- package/dist/utils/virtualDiffLayout.d.ts +40 -1
- package/dist/utils/virtualDiffLayout.d.ts.map +1 -1
- package/dist/utils/virtualDiffLayout.js +96 -1
- package/dist/utils/virtualDiffLayout.js.map +1 -1
- package/dist/worker/WorkerPoolManager.d.ts +7 -2
- package/dist/worker/WorkerPoolManager.d.ts.map +1 -1
- package/dist/worker/WorkerPoolManager.js +83 -18
- package/dist/worker/WorkerPoolManager.js.map +1 -1
- package/dist/worker/index.d.ts +2 -2
- package/dist/worker/types.d.ts +11 -1
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/worker-portable.js +46 -66
- package/dist/worker/worker-portable.js.map +1 -1
- package/dist/worker/worker.js +46 -66
- package/dist/worker/worker.js.map +1 -1
- package/package.json +12 -5
- package/dist/utils/iterateOverFile.d.ts +0 -50
- package/dist/utils/iterateOverFile.d.ts.map +0 -1
- package/dist/utils/iterateOverFile.js +0 -49
- package/dist/utils/iterateOverFile.js.map +0 -1
|
@@ -0,0 +1,1626 @@
|
|
|
1
|
+
import { createSegmenter, endsWithLineBreak, getGraphemeSegmenter } from "./utils.js";
|
|
2
|
+
import { applyDocumentChangeToLineAnnotations } from "./lineAnnotations.js";
|
|
3
|
+
//#region src/editor/selection.ts
|
|
4
|
+
const DirectionBackward = -1;
|
|
5
|
+
const DirectionNone = 0;
|
|
6
|
+
const DirectionForward = 1;
|
|
7
|
+
const AUTO_SURROUND_CLOSE_CHARS = /* @__PURE__ */ new Map([
|
|
8
|
+
["'", "'"],
|
|
9
|
+
["\"", "\""],
|
|
10
|
+
["`", "`"],
|
|
11
|
+
["{", "}"],
|
|
12
|
+
["[", "]"],
|
|
13
|
+
["<", ">"],
|
|
14
|
+
["(", ")"]
|
|
15
|
+
]);
|
|
16
|
+
const AUTO_SURROUND_QUOTE_CHARS = /* @__PURE__ */ new Set([
|
|
17
|
+
"'",
|
|
18
|
+
"\"",
|
|
19
|
+
"`"
|
|
20
|
+
]);
|
|
21
|
+
const AUTO_SURROUND_BRACKET_CHARS = /* @__PURE__ */ new Set([
|
|
22
|
+
"{",
|
|
23
|
+
"[",
|
|
24
|
+
"(",
|
|
25
|
+
"<"
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* Converts a selection from a web selection to an editor selection.
|
|
29
|
+
*/
|
|
30
|
+
function convertSelection(range, direction = 0) {
|
|
31
|
+
const start = boundaryToPosition(range.startContainer, range.startOffset);
|
|
32
|
+
const end = boundaryToPosition(range.endContainer, range.endOffset);
|
|
33
|
+
if (start === null || end === null) return;
|
|
34
|
+
return {
|
|
35
|
+
start,
|
|
36
|
+
end,
|
|
37
|
+
direction
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the indent edits for a selection.
|
|
42
|
+
*/
|
|
43
|
+
function resolveIndentEdits(textDocument, selection, tabSize, outdent) {
|
|
44
|
+
if (textDocument === void 0) return [[], selection];
|
|
45
|
+
const { start, end } = selection;
|
|
46
|
+
const edits = [];
|
|
47
|
+
let newSelection = { ...selection };
|
|
48
|
+
const blockIndent = start.line !== end.line;
|
|
49
|
+
let endLine = end.line;
|
|
50
|
+
if (start.line < end.line && end.character === 0) endLine--;
|
|
51
|
+
for (let line = start.line; line <= endLine; line++) {
|
|
52
|
+
const lineText = textDocument.getLineText(line);
|
|
53
|
+
if (lineText === void 0) continue;
|
|
54
|
+
if (blockIndent && lineText.trim().length === 0) continue;
|
|
55
|
+
const indentUnit = lineText.startsWith(" ") ? " " : " ".repeat(tabSize);
|
|
56
|
+
let deleteLength = 0;
|
|
57
|
+
let newText = indentUnit;
|
|
58
|
+
if (outdent) {
|
|
59
|
+
if (lineText.startsWith(" ")) deleteLength = 1;
|
|
60
|
+
else if (lineText.startsWith(" ")) {
|
|
61
|
+
const leadingSpacesLength = lineText.length - lineText.trimStart().length;
|
|
62
|
+
deleteLength = Math.min(indentUnit.length, leadingSpacesLength);
|
|
63
|
+
}
|
|
64
|
+
if (deleteLength === 0) continue;
|
|
65
|
+
newText = "";
|
|
66
|
+
}
|
|
67
|
+
edits.push({
|
|
68
|
+
range: {
|
|
69
|
+
start: {
|
|
70
|
+
line,
|
|
71
|
+
character: 0
|
|
72
|
+
},
|
|
73
|
+
end: {
|
|
74
|
+
line,
|
|
75
|
+
character: deleteLength
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
newText
|
|
79
|
+
});
|
|
80
|
+
const delta = newText.length - deleteLength;
|
|
81
|
+
if (line === start.line) newSelection = {
|
|
82
|
+
...newSelection,
|
|
83
|
+
start: {
|
|
84
|
+
...start,
|
|
85
|
+
character: Math.max(0, start.character + delta)
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
if (line === end.line) newSelection = {
|
|
89
|
+
...newSelection,
|
|
90
|
+
end: {
|
|
91
|
+
...end,
|
|
92
|
+
character: Math.max(0, end.character + delta)
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return [edits, newSelection];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Maps the cursor move to all selections.
|
|
100
|
+
*/
|
|
101
|
+
function mapCursorMove(textDocument, selections, shortcut, options = {}) {
|
|
102
|
+
const lineCount = textDocument.lineCount;
|
|
103
|
+
return selections.map((selection) => {
|
|
104
|
+
let { line, character } = shortcut === "up" || shortcut === "left" ? selection.start : selection.end;
|
|
105
|
+
if (shortcut === "textStart" || shortcut === "start" || shortcut === "end") {
|
|
106
|
+
const caret = getCaretPosition(selection);
|
|
107
|
+
line = caret.line;
|
|
108
|
+
character = caret.character;
|
|
109
|
+
const softLine = getSoftLineInfo(textDocument, line, character, options);
|
|
110
|
+
if (shortcut === "textStart") {
|
|
111
|
+
const softLineText = textDocument.getLineText(line).slice(softLine.start, softLine.end);
|
|
112
|
+
const indent = softLine.start + getLeadingSpaces(softLineText);
|
|
113
|
+
character = character === indent ? softLine.start : indent;
|
|
114
|
+
} else character = shortcut === "start" ? softLine.start : softLine.end;
|
|
115
|
+
} else if (shortcut === "up") {
|
|
116
|
+
const moved = moveBySoftLine(textDocument, line, character, -1, options);
|
|
117
|
+
if (moved !== void 0) {
|
|
118
|
+
line = moved.line;
|
|
119
|
+
character = moved.character;
|
|
120
|
+
} else if (line > 0) line = options.resolveRenderableLine == null ? line - 1 : options.resolveRenderableLine(line - 1, "up") ?? line;
|
|
121
|
+
} else if (shortcut === "down") {
|
|
122
|
+
const moved = moveBySoftLine(textDocument, line, character, 1, options);
|
|
123
|
+
if (moved !== void 0) {
|
|
124
|
+
line = moved.line;
|
|
125
|
+
character = moved.character;
|
|
126
|
+
} else {
|
|
127
|
+
const maxLine = Math.max(lineCount - 1, 0);
|
|
128
|
+
if (line < maxLine) line = options.resolveRenderableLine == null ? line + 1 : Math.min(options.resolveRenderableLine(line + 1, "down") ?? line, maxLine);
|
|
129
|
+
}
|
|
130
|
+
} else if (isCollapsedSelection(selection)) {
|
|
131
|
+
const lineLength = textDocument.getLineLength(line);
|
|
132
|
+
character = Math.min(character, lineLength);
|
|
133
|
+
if (shortcut === "left") {
|
|
134
|
+
if (character > 0) character = stepCharacterByGrapheme(textDocument, line, character, false);
|
|
135
|
+
else if (line > 0) {
|
|
136
|
+
const targetLine = options.resolveRenderableLine == null ? line - 1 : options.resolveRenderableLine(line - 1, "up");
|
|
137
|
+
if (targetLine !== void 0) {
|
|
138
|
+
line = targetLine;
|
|
139
|
+
character = textDocument.getLineLength(line);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else if (character < lineLength) character = stepCharacterByGrapheme(textDocument, line, character, true);
|
|
143
|
+
else if (line < lineCount - 1) {
|
|
144
|
+
const targetLine = options.resolveRenderableLine == null ? line + 1 : options.resolveRenderableLine(line + 1, "down");
|
|
145
|
+
if (targetLine !== void 0) {
|
|
146
|
+
line = Math.min(targetLine, lineCount - 1);
|
|
147
|
+
character = 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const pos = {
|
|
152
|
+
line,
|
|
153
|
+
character
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
start: pos,
|
|
157
|
+
end: pos,
|
|
158
|
+
direction: 0
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function moveBySoftLine(textDocument, line, character, direction, options) {
|
|
163
|
+
if (options.getSoftLineOffsets === void 0) return;
|
|
164
|
+
const current = getSoftLineInfo(textDocument, line, character, options);
|
|
165
|
+
const targetIndex = current.index + direction;
|
|
166
|
+
let targetLine = line;
|
|
167
|
+
let target;
|
|
168
|
+
if (targetIndex >= 0 && targetIndex < current.count) target = getSoftLineInfoAtIndex(textDocument, targetLine, targetIndex, options);
|
|
169
|
+
else {
|
|
170
|
+
const nextLine = line + direction;
|
|
171
|
+
if (nextLine < 0 || nextLine >= textDocument.lineCount) return {
|
|
172
|
+
line,
|
|
173
|
+
character
|
|
174
|
+
};
|
|
175
|
+
const resolvedLine = options.resolveRenderableLine == null ? nextLine : options.resolveRenderableLine(nextLine, direction < 0 ? "up" : "down");
|
|
176
|
+
if (resolvedLine === void 0) return {
|
|
177
|
+
line,
|
|
178
|
+
character
|
|
179
|
+
};
|
|
180
|
+
targetLine = Math.min(resolvedLine, textDocument.lineCount - 1);
|
|
181
|
+
const targetCount = getSoftLineCount(textDocument, targetLine, options);
|
|
182
|
+
target = getSoftLineInfoAtIndex(textDocument, targetLine, direction < 0 ? targetCount - 1 : 0, options);
|
|
183
|
+
}
|
|
184
|
+
const column = Math.max(0, character - current.start);
|
|
185
|
+
const targetCharacter = target.start + column;
|
|
186
|
+
const landedCharacter = target.index === target.count - 1 ? targetCharacter : Math.min(targetCharacter, target.end);
|
|
187
|
+
const targetLineText = textDocument.getLineText(targetLine);
|
|
188
|
+
return {
|
|
189
|
+
line: targetLine,
|
|
190
|
+
character: landedCharacter > targetLineText.length ? landedCharacter : snapCharacterToGraphemeBoundary(targetLineText, landedCharacter)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function snapCharacterToGraphemeBoundary(lineText, character) {
|
|
194
|
+
if (character <= 0 || character >= lineText.length) return character;
|
|
195
|
+
const segmenter = getGraphemeSegmenter();
|
|
196
|
+
if (segmenter !== void 0) {
|
|
197
|
+
for (const segment of segmenter.segment(lineText)) {
|
|
198
|
+
if (character <= segment.index) return character;
|
|
199
|
+
const segmentEnd = segment.index + segment.segment.length;
|
|
200
|
+
if (character < segmentEnd) return segmentEnd;
|
|
201
|
+
}
|
|
202
|
+
return character;
|
|
203
|
+
}
|
|
204
|
+
let segmentStart = 0;
|
|
205
|
+
for (const codePoint of lineText) {
|
|
206
|
+
if (character <= segmentStart) return character;
|
|
207
|
+
const segmentEnd = segmentStart + codePoint.length;
|
|
208
|
+
if (character < segmentEnd) return segmentEnd;
|
|
209
|
+
segmentStart = segmentEnd;
|
|
210
|
+
}
|
|
211
|
+
return character;
|
|
212
|
+
}
|
|
213
|
+
function getSoftLineInfo(textDocument, line, character, options) {
|
|
214
|
+
const lineLength = textDocument.getLineLength(line);
|
|
215
|
+
const offsets = options.getSoftLineOffsets?.(line);
|
|
216
|
+
if (offsets === void 0 || offsets.length < 2) return {
|
|
217
|
+
start: 0,
|
|
218
|
+
end: lineLength,
|
|
219
|
+
index: 0,
|
|
220
|
+
count: 1
|
|
221
|
+
};
|
|
222
|
+
for (let index = 0; index + 1 < offsets.length; index++) {
|
|
223
|
+
const softLine = getSoftLineInfoAtIndex(textDocument, line, index, options);
|
|
224
|
+
if (character >= softLine.start && character <= softLine.end) return softLine;
|
|
225
|
+
}
|
|
226
|
+
return getSoftLineInfoAtIndex(textDocument, line, character < (offsets[0] ?? 0) ? 0 : offsets.length - 2, options);
|
|
227
|
+
}
|
|
228
|
+
function getSoftLineCount(textDocument, line, options) {
|
|
229
|
+
const offsets = options.getSoftLineOffsets?.(line);
|
|
230
|
+
return offsets === void 0 || offsets.length < 2 ? 1 : offsets.length - 1;
|
|
231
|
+
}
|
|
232
|
+
function getSoftLineInfoAtIndex(textDocument, line, index, options) {
|
|
233
|
+
const lineLength = textDocument.getLineLength(line);
|
|
234
|
+
const offsets = options.getSoftLineOffsets?.(line);
|
|
235
|
+
if (offsets === void 0 || offsets.length < 2) return {
|
|
236
|
+
start: 0,
|
|
237
|
+
end: lineLength,
|
|
238
|
+
index: 0,
|
|
239
|
+
count: 1
|
|
240
|
+
};
|
|
241
|
+
const count = offsets.length - 1;
|
|
242
|
+
const boundedIndex = Math.max(0, Math.min(index, count - 1));
|
|
243
|
+
const start = Math.max(0, Math.min(lineLength, offsets[boundedIndex] ?? 0));
|
|
244
|
+
return {
|
|
245
|
+
start,
|
|
246
|
+
end: Math.max(start, Math.min(lineLength, offsets[boundedIndex + 1] ?? lineLength)),
|
|
247
|
+
index: boundedIndex,
|
|
248
|
+
count
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Same as mapCursorMove, but with shift key pressed.
|
|
253
|
+
*/
|
|
254
|
+
function mapSelectionShift(textDocument, selections, shortcut, options = {}) {
|
|
255
|
+
return selections.map((selection) => {
|
|
256
|
+
const focusPosition = selection.direction === -1 ? selection.start : selection.end;
|
|
257
|
+
const [movedFocusSelection] = mapCursorMove(textDocument, [{
|
|
258
|
+
start: focusPosition,
|
|
259
|
+
end: focusPosition,
|
|
260
|
+
direction: 0
|
|
261
|
+
}], shortcut, options);
|
|
262
|
+
return createSelectionFrom(selection, movedFocusSelection);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Applies a text change to the given text document
|
|
267
|
+
*/
|
|
268
|
+
function applyTextChangeToSelections(textDocument, selections, edit, lineAnnotations, tabSize = 2, undoBoundary = false) {
|
|
269
|
+
if (selections[selections.length - 1] === void 0) return { nextSelections: [] };
|
|
270
|
+
const selectionPositions = [];
|
|
271
|
+
for (const selection of selections) selectionPositions.push(selection.start, selection.end);
|
|
272
|
+
const selectionOffsets = selectionPositions.map((position) => textDocument.offsetAt(position));
|
|
273
|
+
const primaryStartOffset = selectionOffsets[(selections.length - 1) * 2];
|
|
274
|
+
const primaryEndOffset = selectionOffsets[(selections.length - 1) * 2 + 1];
|
|
275
|
+
const ordered = [];
|
|
276
|
+
let isAlreadyOrdered = true;
|
|
277
|
+
for (let index = 0; index < selections.length; index++) {
|
|
278
|
+
const entry = {
|
|
279
|
+
index,
|
|
280
|
+
start: selectionOffsets[index * 2],
|
|
281
|
+
end: selectionOffsets[index * 2 + 1]
|
|
282
|
+
};
|
|
283
|
+
const previous = ordered[ordered.length - 1];
|
|
284
|
+
if (previous !== void 0 && (entry.start < previous.start || entry.start === previous.start && entry.end < previous.end)) isAlreadyOrdered = false;
|
|
285
|
+
ordered.push(entry);
|
|
286
|
+
}
|
|
287
|
+
if (!isAlreadyOrdered) ordered.sort((a, b) => {
|
|
288
|
+
const startOrder = a.start - b.start;
|
|
289
|
+
if (startOrder !== 0) return startOrder;
|
|
290
|
+
const endOrder = a.end - b.end;
|
|
291
|
+
if (endOrder !== 0) return endOrder;
|
|
292
|
+
return a.index - b.index;
|
|
293
|
+
});
|
|
294
|
+
const adjustedChange = normalizeLeadingIndentForChange(textDocument, edit, tabSize);
|
|
295
|
+
const edits = [];
|
|
296
|
+
const nextSelectionOffsets = Array.from({ length: selections.length });
|
|
297
|
+
let offsetDelta = 0;
|
|
298
|
+
let mergedGroup;
|
|
299
|
+
const finalizeMergedGroup = () => {
|
|
300
|
+
if (mergedGroup === void 0) return;
|
|
301
|
+
const perGroupChange = normalizeLeadingIndentForChange(textDocument, {
|
|
302
|
+
start: mergedGroup.start,
|
|
303
|
+
end: mergedGroup.end,
|
|
304
|
+
text: adjustedChange.text
|
|
305
|
+
}, tabSize);
|
|
306
|
+
const newText = expandSingleNewlineInsert(textDocument, perGroupChange.text, perGroupChange.start);
|
|
307
|
+
edits.push({
|
|
308
|
+
start: perGroupChange.start,
|
|
309
|
+
end: perGroupChange.end,
|
|
310
|
+
text: newText
|
|
311
|
+
});
|
|
312
|
+
const nextOffsets = [mergedGroup.start + offsetDelta + newText.length, mergedGroup.start + offsetDelta + newText.length];
|
|
313
|
+
for (const index of mergedGroup.indices) nextSelectionOffsets[index] = nextOffsets;
|
|
314
|
+
offsetDelta += newText.length - (perGroupChange.end - perGroupChange.start);
|
|
315
|
+
mergedGroup = void 0;
|
|
316
|
+
};
|
|
317
|
+
for (const entry of ordered) {
|
|
318
|
+
const startOffset = Math.max(0, entry.start + (adjustedChange.start - primaryStartOffset));
|
|
319
|
+
const endOffset = Math.max(startOffset, entry.end + (adjustedChange.end - primaryEndOffset));
|
|
320
|
+
if (mergedGroup !== void 0 && startOffset < mergedGroup.end) {
|
|
321
|
+
mergedGroup.end = Math.max(mergedGroup.end, endOffset);
|
|
322
|
+
mergedGroup.indices.push(entry.index);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
finalizeMergedGroup();
|
|
326
|
+
mergedGroup = {
|
|
327
|
+
start: startOffset,
|
|
328
|
+
end: endOffset,
|
|
329
|
+
indices: [entry.index]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
finalizeMergedGroup();
|
|
333
|
+
const change = textDocument.applyResolvedEdits(edits, true, selections, void 0, undoBoundary);
|
|
334
|
+
const nextSelections = createSelectionsFromOffsetPairs(textDocument, nextSelectionOffsets.map((offsets) => {
|
|
335
|
+
if (offsets === void 0) throw new Error("Missing next selection offsets");
|
|
336
|
+
return offsets;
|
|
337
|
+
}));
|
|
338
|
+
textDocument.setLastUndoSelectionsAfter(nextSelections);
|
|
339
|
+
if (change !== void 0 && lineAnnotations !== void 0) {
|
|
340
|
+
const nextLineAnnotations = applyDocumentChangeToLineAnnotations(change, lineAnnotations);
|
|
341
|
+
if (nextLineAnnotations !== void 0) textDocument.setLastUndoLineAnnotations(lineAnnotations, nextLineAnnotations);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
nextSelections,
|
|
345
|
+
change
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Returns the next anchor/focus offsets after replacing a selection range.
|
|
350
|
+
* When the inserted text still contains the original selection (auto-surround),
|
|
351
|
+
* the inner range is reselected to match VS Code/CodeMirror behavior.
|
|
352
|
+
*/
|
|
353
|
+
function getAutoSurroundPreservedOffset(originalText, newText) {
|
|
354
|
+
if (newText.length !== originalText.length + 2) return;
|
|
355
|
+
const closeChar = AUTO_SURROUND_CLOSE_CHARS.get(newText[0]);
|
|
356
|
+
if (closeChar === void 0 || newText[newText.length - 1] !== closeChar || newText.slice(1, -1) !== originalText) return;
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
function getNextSelectionOffsetPairAfterReplace(textDocument, entry, offsetDelta, newText) {
|
|
360
|
+
const insertStart = entry.start + offsetDelta;
|
|
361
|
+
const insertEnd = insertStart + newText.length;
|
|
362
|
+
if (entry.end - entry.start > 0) {
|
|
363
|
+
const originalText = textDocument.getTextSlice(entry.start, entry.end);
|
|
364
|
+
const preservedOffset = getAutoSurroundPreservedOffset(originalText, newText) ?? newText.indexOf(originalText);
|
|
365
|
+
if (preservedOffset !== -1 && preservedOffset + originalText.length <= newText.length) {
|
|
366
|
+
const rangeStart = insertStart + preservedOffset;
|
|
367
|
+
return [rangeStart, rangeStart + originalText.length];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return [insertEnd, insertEnd];
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Applies text replacements to multiple selections. Texts pair by selection
|
|
374
|
+
* index unless they are explicitly marked as document ordered.
|
|
375
|
+
*/
|
|
376
|
+
function applyTextReplaceToSelections(textDocument, selections, texts, lineAnnotations, undoBoundary = false, textOrder = "selection") {
|
|
377
|
+
if (selections.length !== texts.length) throw new Error("Selection text replacements must match the selection count");
|
|
378
|
+
const selectionPositions = [];
|
|
379
|
+
for (const selection of selections) selectionPositions.push(selection.start, selection.end);
|
|
380
|
+
const selectionOffsets = selectionPositions.map((position) => textDocument.offsetAt(position));
|
|
381
|
+
const ordered = [];
|
|
382
|
+
let isAlreadyOrdered = true;
|
|
383
|
+
for (let index = 0; index < selections.length; index++) {
|
|
384
|
+
const entry = {
|
|
385
|
+
index,
|
|
386
|
+
start: selectionOffsets[index * 2],
|
|
387
|
+
end: selectionOffsets[index * 2 + 1]
|
|
388
|
+
};
|
|
389
|
+
const previous = ordered[ordered.length - 1];
|
|
390
|
+
if (previous !== void 0 && (entry.start < previous.start || entry.start === previous.start && entry.end < previous.end)) isAlreadyOrdered = false;
|
|
391
|
+
ordered.push(entry);
|
|
392
|
+
}
|
|
393
|
+
if (!isAlreadyOrdered) ordered.sort((a, b) => {
|
|
394
|
+
const startOrder = a.start - b.start;
|
|
395
|
+
if (startOrder !== 0) return startOrder;
|
|
396
|
+
const endOrder = a.end - b.end;
|
|
397
|
+
if (endOrder !== 0) return endOrder;
|
|
398
|
+
return a.index - b.index;
|
|
399
|
+
});
|
|
400
|
+
const allDeletes = texts.every((text) => text === "");
|
|
401
|
+
let edits;
|
|
402
|
+
const nextSelectionOffsetPairs = Array.from({ length: selections.length });
|
|
403
|
+
if (allDeletes) {
|
|
404
|
+
edits = [];
|
|
405
|
+
let hasEffect = false;
|
|
406
|
+
for (const entry of ordered) {
|
|
407
|
+
nextSelectionOffsetPairs[entry.index] = [entry.end, entry.end];
|
|
408
|
+
if (entry.start >= entry.end) continue;
|
|
409
|
+
hasEffect = true;
|
|
410
|
+
const last = edits[edits.length - 1];
|
|
411
|
+
if (last !== void 0 && entry.start < last.end) edits[edits.length - 1] = {
|
|
412
|
+
start: last.start,
|
|
413
|
+
end: Math.max(last.end, entry.end),
|
|
414
|
+
text: ""
|
|
415
|
+
};
|
|
416
|
+
else edits.push({
|
|
417
|
+
start: entry.start,
|
|
418
|
+
end: entry.end,
|
|
419
|
+
text: ""
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (!hasEffect) return { nextSelections: selections };
|
|
423
|
+
for (const entry of ordered) {
|
|
424
|
+
const caret = entry.end;
|
|
425
|
+
let delta = 0;
|
|
426
|
+
let next = caret;
|
|
427
|
+
for (const edit of edits) {
|
|
428
|
+
if (caret <= edit.start) break;
|
|
429
|
+
if (caret >= edit.end) {
|
|
430
|
+
delta -= edit.end - edit.start;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
next = edit.start + delta;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
if (next === caret) next += delta;
|
|
437
|
+
nextSelectionOffsetPairs[entry.index] = [next, next];
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
edits = [];
|
|
441
|
+
let offsetDelta = 0;
|
|
442
|
+
let previousEditEnd = -1;
|
|
443
|
+
for (let index = 0; index < ordered.length; index++) {
|
|
444
|
+
const entry = ordered[index];
|
|
445
|
+
if (entry.start < previousEditEnd) throw new Error("Overlapping multi-selection edits are not supported");
|
|
446
|
+
previousEditEnd = entry.end;
|
|
447
|
+
const newText = expandSingleNewlineInsert(textDocument, texts[textOrder === "document" ? index : entry.index], entry.start);
|
|
448
|
+
edits.push({
|
|
449
|
+
start: entry.start,
|
|
450
|
+
end: entry.end,
|
|
451
|
+
text: newText
|
|
452
|
+
});
|
|
453
|
+
nextSelectionOffsetPairs[entry.index] = getNextSelectionOffsetPairAfterReplace(textDocument, entry, offsetDelta, newText);
|
|
454
|
+
offsetDelta += newText.length - (entry.end - entry.start);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const change = textDocument.applyResolvedEdits(edits, true, selections, void 0, undoBoundary);
|
|
458
|
+
const nextSelections = createSelectionsFromOffsetPairs(textDocument, nextSelectionOffsetPairs.map((offsets) => {
|
|
459
|
+
if (offsets === void 0) throw new Error("Missing next selection offsets");
|
|
460
|
+
return offsets;
|
|
461
|
+
}));
|
|
462
|
+
textDocument.setLastUndoSelectionsAfter(nextSelections);
|
|
463
|
+
if (change !== void 0 && lineAnnotations !== void 0) {
|
|
464
|
+
const nextLineAnnotations = applyDocumentChangeToLineAnnotations(change, lineAnnotations);
|
|
465
|
+
if (nextLineAnnotations !== void 0) textDocument.setLastUndoLineAnnotations(lineAnnotations, nextLineAnnotations);
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
nextSelections,
|
|
469
|
+
change
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function shouldAutoSurroundChar(autoSurround, char) {
|
|
473
|
+
if (autoSurround === "never") return false;
|
|
474
|
+
if (autoSurround === "brackets") return AUTO_SURROUND_BRACKET_CHARS.has(char);
|
|
475
|
+
if (autoSurround === "quotes") return AUTO_SURROUND_QUOTE_CHARS.has(char);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Returns per-selection replacement text when typing a surround character over
|
|
480
|
+
* non-collapsed selections, matching VS Code auto-surround behavior.
|
|
481
|
+
*/
|
|
482
|
+
function getAutoSurroundReplacementTexts(textDocument, selections, char, autoSurround) {
|
|
483
|
+
if (char.length !== 1 || selections.length === 0) return;
|
|
484
|
+
const closeChar = AUTO_SURROUND_CLOSE_CHARS.get(char);
|
|
485
|
+
if (closeChar === void 0 || !shouldAutoSurroundChar(autoSurround, char)) return;
|
|
486
|
+
const replacements = [];
|
|
487
|
+
for (const selection of selections) {
|
|
488
|
+
if (isCollapsedSelection(selection)) return;
|
|
489
|
+
replacements.push(char + textDocument.getText(selection) + closeChar);
|
|
490
|
+
}
|
|
491
|
+
return replacements;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Swaps the two characters adjacent to a collapsed selection, matching browser
|
|
495
|
+
* insertTranspose (Ctrl+T) behavior.
|
|
496
|
+
*/
|
|
497
|
+
function applyTransposeToSelections(textDocument, selections, lineAnnotations) {
|
|
498
|
+
const edits = [];
|
|
499
|
+
const nextOffsetPairs = [];
|
|
500
|
+
for (const selection of selections) {
|
|
501
|
+
const [anchor, focus] = getSelectionAnchorAndFocusOffsets(textDocument, selection);
|
|
502
|
+
if (!isCollapsedSelection(selection)) {
|
|
503
|
+
nextOffsetPairs.push([anchor, focus]);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const { line, character } = selection.start;
|
|
507
|
+
const offset = anchor;
|
|
508
|
+
const lineText = textDocument.getLineText(line);
|
|
509
|
+
const lineLength = lineText.length;
|
|
510
|
+
const lineStart = offset - character;
|
|
511
|
+
const graphemeStarts = getLineGraphemeStarts(lineText);
|
|
512
|
+
let edit;
|
|
513
|
+
if (character > 0 && character < lineLength) {
|
|
514
|
+
const before = findClusterBreak(lineText, character, false, graphemeStarts);
|
|
515
|
+
const after = findClusterBreak(lineText, character, true, graphemeStarts);
|
|
516
|
+
edit = {
|
|
517
|
+
start: lineStart + before,
|
|
518
|
+
end: lineStart + after,
|
|
519
|
+
text: lineText.slice(character, after) + lineText.slice(before, character)
|
|
520
|
+
};
|
|
521
|
+
nextOffsetPairs.push([lineStart + after, lineStart + after]);
|
|
522
|
+
} else if (character === lineLength && graphemeStarts.length >= 2) {
|
|
523
|
+
const lastStart = graphemeStarts[graphemeStarts.length - 1];
|
|
524
|
+
const secondLastStart = graphemeStarts[graphemeStarts.length - 2];
|
|
525
|
+
edit = {
|
|
526
|
+
start: lineStart + secondLastStart,
|
|
527
|
+
end: offset,
|
|
528
|
+
text: lineText.slice(lastStart, lineLength) + lineText.slice(secondLastStart, lastStart)
|
|
529
|
+
};
|
|
530
|
+
nextOffsetPairs.push([offset, offset]);
|
|
531
|
+
} else if (character === 0 && line > 0 && lineLength > 0) {
|
|
532
|
+
const prevLine = line - 1;
|
|
533
|
+
const prevLineText = textDocument.getLineText(prevLine);
|
|
534
|
+
const prevLength = prevLineText.length;
|
|
535
|
+
const prevEnd = textDocument.offsetAt({
|
|
536
|
+
line: prevLine,
|
|
537
|
+
character: prevLength
|
|
538
|
+
});
|
|
539
|
+
const prevGraphemeStart = prevLength > 0 ? findClusterBreak(prevLineText, prevLength, false, getLineGraphemeStarts(prevLineText)) : prevLength;
|
|
540
|
+
const firstEnd = findClusterBreak(lineText, 0, true, graphemeStarts);
|
|
541
|
+
const prevStart = prevEnd - (prevLength - prevGraphemeStart);
|
|
542
|
+
const newText = lineText.slice(0, firstEnd) + textDocument.getTextSlice(prevEnd, offset) + prevLineText.slice(prevGraphemeStart, prevLength);
|
|
543
|
+
edit = {
|
|
544
|
+
start: prevStart,
|
|
545
|
+
end: offset + firstEnd,
|
|
546
|
+
text: newText
|
|
547
|
+
};
|
|
548
|
+
const caret = prevStart + newText.length;
|
|
549
|
+
nextOffsetPairs.push([caret, caret]);
|
|
550
|
+
} else {
|
|
551
|
+
nextOffsetPairs.push([anchor, focus]);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
edits.push(edit);
|
|
555
|
+
}
|
|
556
|
+
if (edits.length === 0) return { nextSelections: selections };
|
|
557
|
+
edits.sort((a, b) => a.start - b.start);
|
|
558
|
+
for (let index = 1; index < edits.length; index++) if (edits[index].start < edits[index - 1].end) throw new Error("Overlapping multi-selection edits are not supported");
|
|
559
|
+
const change = textDocument.applyResolvedEdits(edits, true, selections);
|
|
560
|
+
const nextSelections = createSelectionsFromOffsetPairs(textDocument, nextOffsetPairs);
|
|
561
|
+
textDocument.setLastUndoSelectionsAfter(nextSelections);
|
|
562
|
+
if (change !== void 0 && lineAnnotations !== void 0) {
|
|
563
|
+
const nextLineAnnotations = applyDocumentChangeToLineAnnotations(change, lineAnnotations);
|
|
564
|
+
if (nextLineAnnotations !== void 0) textDocument.setLastUndoLineAnnotations(lineAnnotations, nextLineAnnotations);
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
nextSelections,
|
|
568
|
+
change
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Deletes from each selection to the end of its line, including the line break
|
|
573
|
+
* when the caret is already at the end of a non-final line. Non-collapsed
|
|
574
|
+
* selections delete their selected text instead.
|
|
575
|
+
*/
|
|
576
|
+
function applyDeleteHardLineForwardToSelections(textDocument, selections, lineAnnotations) {
|
|
577
|
+
const deleteSelections = selections.map((selection) => {
|
|
578
|
+
const range = resolveDeleteHardLineForwardRange(textDocument, selection);
|
|
579
|
+
return {
|
|
580
|
+
start: range.start,
|
|
581
|
+
end: range.end,
|
|
582
|
+
direction: 0
|
|
583
|
+
};
|
|
584
|
+
});
|
|
585
|
+
return applyTextReplaceToSelections(textDocument, deleteSelections, deleteSelections.map(() => ""), lineAnnotations);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Deletes from each selection back to the start of its soft (visual) line.
|
|
589
|
+
* Non-collapsed selections delete their selected text instead.
|
|
590
|
+
*/
|
|
591
|
+
function applyDeleteSoftLineBackwardToSelections(textDocument, selections, getSoftLineStart, lineAnnotations) {
|
|
592
|
+
const deleteSelections = selections.map((selection) => {
|
|
593
|
+
if (!isCollapsedSelection(selection)) return {
|
|
594
|
+
start: selection.start,
|
|
595
|
+
end: selection.end,
|
|
596
|
+
direction: 0
|
|
597
|
+
};
|
|
598
|
+
const caret = getCaretPosition(selection);
|
|
599
|
+
const { line, character } = caret;
|
|
600
|
+
const softLineStart = getSoftLineStart?.(line, character) ?? 0;
|
|
601
|
+
if (character > softLineStart) return {
|
|
602
|
+
start: {
|
|
603
|
+
line,
|
|
604
|
+
character: softLineStart
|
|
605
|
+
},
|
|
606
|
+
end: {
|
|
607
|
+
line,
|
|
608
|
+
character
|
|
609
|
+
},
|
|
610
|
+
direction: 0
|
|
611
|
+
};
|
|
612
|
+
if (line === 0) return {
|
|
613
|
+
start: caret,
|
|
614
|
+
end: caret,
|
|
615
|
+
direction: 0
|
|
616
|
+
};
|
|
617
|
+
const prevLineLength = textDocument.getLineLength(line - 1);
|
|
618
|
+
return {
|
|
619
|
+
start: {
|
|
620
|
+
line: line - 1,
|
|
621
|
+
character: prevLineLength
|
|
622
|
+
},
|
|
623
|
+
end: {
|
|
624
|
+
line,
|
|
625
|
+
character: 0
|
|
626
|
+
},
|
|
627
|
+
direction: 0
|
|
628
|
+
};
|
|
629
|
+
});
|
|
630
|
+
return applyTextReplaceToSelections(textDocument, deleteSelections, deleteSelections.map(() => ""), lineAnnotations);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Deletes the word or separator group immediately before each selection.
|
|
634
|
+
* Non-collapsed selections delete their selected text instead.
|
|
635
|
+
*/
|
|
636
|
+
function applyDeleteWordBackwardToSelections(textDocument, selections, lineAnnotations) {
|
|
637
|
+
const deleteSelections = selections.map((selection) => {
|
|
638
|
+
const [start, end] = resolveDeleteWordBackwardRange(textDocument, selection);
|
|
639
|
+
return {
|
|
640
|
+
start,
|
|
641
|
+
end,
|
|
642
|
+
direction: 0
|
|
643
|
+
};
|
|
644
|
+
});
|
|
645
|
+
return applyTextReplaceToSelections(textDocument, deleteSelections, deleteSelections.map(() => ""), lineAnnotations);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Resolves the document range deleted by Backspace or Delete at a collapsed
|
|
649
|
+
* caret. Non-collapsed selections delete their selected text instead.
|
|
650
|
+
*/
|
|
651
|
+
function resolveDeleteCharacterRange(textDocument, selection, forward) {
|
|
652
|
+
if (!isCollapsedSelection(selection)) return [selection.start, selection.end];
|
|
653
|
+
const caret = getCaretPosition(selection);
|
|
654
|
+
let { line, character } = caret;
|
|
655
|
+
const lineLength = textDocument.getLineLength(line);
|
|
656
|
+
const lineCount = textDocument.lineCount;
|
|
657
|
+
character = Math.min(character, lineLength);
|
|
658
|
+
if (forward) {
|
|
659
|
+
if (character < lineLength) return [{
|
|
660
|
+
line,
|
|
661
|
+
character
|
|
662
|
+
}, {
|
|
663
|
+
line,
|
|
664
|
+
character: stepCharacterByGrapheme(textDocument, line, character, true)
|
|
665
|
+
}];
|
|
666
|
+
if (line < lineCount - 1) return [{
|
|
667
|
+
line,
|
|
668
|
+
character: lineLength
|
|
669
|
+
}, {
|
|
670
|
+
line: line + 1,
|
|
671
|
+
character: 0
|
|
672
|
+
}];
|
|
673
|
+
return [caret, caret];
|
|
674
|
+
}
|
|
675
|
+
if (character > 0) return [{
|
|
676
|
+
line,
|
|
677
|
+
character: stepCharacterByGrapheme(textDocument, line, character, false)
|
|
678
|
+
}, {
|
|
679
|
+
line,
|
|
680
|
+
character
|
|
681
|
+
}];
|
|
682
|
+
if (line > 0) {
|
|
683
|
+
const prevLineLength = textDocument.getLineLength(line - 1);
|
|
684
|
+
return [{
|
|
685
|
+
line: line - 1,
|
|
686
|
+
character: prevLineLength
|
|
687
|
+
}, {
|
|
688
|
+
line,
|
|
689
|
+
character: 0
|
|
690
|
+
}];
|
|
691
|
+
}
|
|
692
|
+
return [caret, caret];
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Deletes one grapheme (or selected text) at each selection.
|
|
696
|
+
*/
|
|
697
|
+
function applyDeleteCharacterToSelections(textDocument, selections, forward, lineAnnotations, tabSize = 2) {
|
|
698
|
+
const deleteSelections = selections.map((selection) => {
|
|
699
|
+
let [start, end] = resolveDeleteCharacterRange(textDocument, selection, forward);
|
|
700
|
+
if (!forward && isCollapsedSelection(selection)) {
|
|
701
|
+
const normalized = normalizeLeadingIndentForChange(textDocument, {
|
|
702
|
+
start: textDocument.offsetAt(start),
|
|
703
|
+
end: textDocument.offsetAt(end),
|
|
704
|
+
text: ""
|
|
705
|
+
}, tabSize);
|
|
706
|
+
start = textDocument.positionAt(normalized.start);
|
|
707
|
+
end = textDocument.positionAt(normalized.end);
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
start,
|
|
711
|
+
end,
|
|
712
|
+
direction: 0
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
return applyTextReplaceToSelections(textDocument, deleteSelections, deleteSelections.map(() => ""), lineAnnotations);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Checks if a selection is collapsed.
|
|
719
|
+
*/
|
|
720
|
+
function isCollapsedSelection(selection) {
|
|
721
|
+
return selection.start.line === selection.end.line && selection.start.character === selection.end.character;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Returns the caret (focus) position for a selection.
|
|
725
|
+
*/
|
|
726
|
+
function getCaretPosition(selection) {
|
|
727
|
+
const { start, end, direction } = selection;
|
|
728
|
+
return direction === -1 ? start : end;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Checks if a line is editable.
|
|
732
|
+
*/
|
|
733
|
+
function isLineEditable(lineType) {
|
|
734
|
+
return lineType === "context" || lineType === "context-expanded" || lineType === "change-addition";
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Checks whether selections `a` and `b` intersect.
|
|
738
|
+
*/
|
|
739
|
+
function selectionIntersects(a, b) {
|
|
740
|
+
const aCollapsed = isCollapsedSelection(a);
|
|
741
|
+
const bCollapsed = isCollapsedSelection(b);
|
|
742
|
+
if (aCollapsed && bCollapsed) return comparePosition(a.start, b.start) === 0;
|
|
743
|
+
if (aCollapsed) return comparePosition(b.start, a.start) <= 0 && comparePosition(a.start, b.end) <= 0;
|
|
744
|
+
if (bCollapsed) return comparePosition(a.start, b.start) <= 0 && comparePosition(b.start, a.end) <= 0;
|
|
745
|
+
return comparePosition(a.start, b.end) < 0 && comparePosition(b.start, a.end) < 0;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Compares two positions.
|
|
749
|
+
*/
|
|
750
|
+
function comparePosition(a, b) {
|
|
751
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
752
|
+
return a.character - b.character;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Creates a selection from anchor and focus offsets.
|
|
756
|
+
*/
|
|
757
|
+
function createSelectionFromAnchorAndFocusOffsets(textDocument, anchorOffset, focusOffset) {
|
|
758
|
+
const direction = anchorOffset === focusOffset ? 0 : anchorOffset < focusOffset ? 1 : -1;
|
|
759
|
+
const start = Math.min(anchorOffset, focusOffset);
|
|
760
|
+
const end = Math.max(anchorOffset, focusOffset);
|
|
761
|
+
return {
|
|
762
|
+
start: textDocument.positionAt(start),
|
|
763
|
+
end: textDocument.positionAt(end),
|
|
764
|
+
direction
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Maps a single offset from the pre-edit document into the post-edit document.
|
|
769
|
+
* `edits` are resolved edits in pre-edit offsets, sorted ascending and
|
|
770
|
+
* non-overlapping. An offset at or after an edit's start shifts to the end of
|
|
771
|
+
* that edit's replacement (right gravity), so text inserted at the caret pushes
|
|
772
|
+
* the caret past it; an offset strictly before an edit is only shifted by the
|
|
773
|
+
* net length change of the edits that precede it.
|
|
774
|
+
*/
|
|
775
|
+
function remapOffsetThroughEdits(offset, edits) {
|
|
776
|
+
let delta = 0;
|
|
777
|
+
for (const edit of edits) {
|
|
778
|
+
if (offset < edit.start) break;
|
|
779
|
+
if (offset >= edit.end) delta += edit.text.length - (edit.end - edit.start);
|
|
780
|
+
else return edit.start + delta + edit.text.length;
|
|
781
|
+
}
|
|
782
|
+
return offset + delta;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Re-anchors selections after a batch of text edits has been applied, so the
|
|
786
|
+
* caret keeps pointing at the same logical location in the changed buffer.
|
|
787
|
+
*
|
|
788
|
+
* `selectionOffsets` (one `[start, end]` pair per selection) and `edits` are
|
|
789
|
+
* measured in the PRE-edit document; the returned selections are built from
|
|
790
|
+
* `textDocument`, which must already reflect the applied edits. Selection
|
|
791
|
+
* direction is preserved by remapping each edge and re-deriving anchor/focus.
|
|
792
|
+
*/
|
|
793
|
+
function remapSelectionsAfterEdits(textDocument, selections, selectionOffsets, edits) {
|
|
794
|
+
return selections.map((selection, index) => {
|
|
795
|
+
const [startOffset, endOffset] = selectionOffsets[index];
|
|
796
|
+
const nextStart = remapOffsetThroughEdits(startOffset, edits);
|
|
797
|
+
const nextEnd = remapOffsetThroughEdits(endOffset, edits);
|
|
798
|
+
return createSelectionFromAnchorAndFocusOffsets(textDocument, selection.direction === -1 ? nextEnd : nextStart, selection.direction === -1 ? nextStart : nextEnd);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Creates a selection from a anchor and focus selection.
|
|
803
|
+
*/
|
|
804
|
+
function createSelectionFrom(anchorSelection, focusSelection) {
|
|
805
|
+
const anchor = anchorSelection.direction === -1 ? anchorSelection.end : anchorSelection.start;
|
|
806
|
+
const currentStartOrder = comparePosition(anchor, focusSelection.start);
|
|
807
|
+
const currentEndOrder = comparePosition(anchor, focusSelection.end);
|
|
808
|
+
let focus = focusSelection.end;
|
|
809
|
+
if (currentStartOrder <= 0) focus = focusSelection.end;
|
|
810
|
+
else if (currentEndOrder >= 0) focus = focusSelection.start;
|
|
811
|
+
else focus = currentStartOrder === 0 ? focusSelection.end : focusSelection.start;
|
|
812
|
+
const anchorVsFocus = comparePosition(anchor, focus);
|
|
813
|
+
return {
|
|
814
|
+
start: anchorVsFocus <= 0 ? anchor : focus,
|
|
815
|
+
end: anchorVsFocus <= 0 ? focus : anchor,
|
|
816
|
+
direction: anchorVsFocus === 0 ? 0 : anchorVsFocus < 0 ? 1 : -1
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Extends or shrinks the selection `original` using the endpoints of `target`, \
|
|
821
|
+
* matching contenteditable shift + click extend behavior.
|
|
822
|
+
*/
|
|
823
|
+
function extendSelection(original, target) {
|
|
824
|
+
const leftExtended = comparePosition(target.start, original.start) < 0;
|
|
825
|
+
const rightExtended = comparePosition(target.end, original.end) > 0;
|
|
826
|
+
if (leftExtended && !rightExtended) return {
|
|
827
|
+
start: target.start,
|
|
828
|
+
end: original.end,
|
|
829
|
+
direction: -1
|
|
830
|
+
};
|
|
831
|
+
if (rightExtended && !leftExtended) return {
|
|
832
|
+
start: original.start,
|
|
833
|
+
end: target.end,
|
|
834
|
+
direction: 1
|
|
835
|
+
};
|
|
836
|
+
if (original.direction === -1) return {
|
|
837
|
+
start: target.start,
|
|
838
|
+
end: original.end,
|
|
839
|
+
direction: comparePosition(target.start, original.end) === 0 ? 0 : -1
|
|
840
|
+
};
|
|
841
|
+
return {
|
|
842
|
+
start: original.start,
|
|
843
|
+
end: target.end,
|
|
844
|
+
direction: comparePosition(original.start, target.end) === 0 ? 0 : 1
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Extends multiple selections.
|
|
849
|
+
*/
|
|
850
|
+
function extendSelections(selections, target) {
|
|
851
|
+
return mergeOverlappingSelections(selections.map((selection) => {
|
|
852
|
+
return extendSelection(selection, target);
|
|
853
|
+
}));
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Merges overlapping selections.
|
|
857
|
+
*/
|
|
858
|
+
function mergeOverlappingSelections(selections) {
|
|
859
|
+
if (selections.length <= 1) return selections;
|
|
860
|
+
const ordered = selections.map((selection, index) => ({
|
|
861
|
+
index,
|
|
862
|
+
selection
|
|
863
|
+
})).sort((a, b) => {
|
|
864
|
+
const startOrder = comparePosition(a.selection.start, b.selection.start);
|
|
865
|
+
if (startOrder !== 0) return startOrder;
|
|
866
|
+
const endOrder = comparePosition(a.selection.end, b.selection.end);
|
|
867
|
+
return endOrder !== 0 ? endOrder : a.index - b.index;
|
|
868
|
+
});
|
|
869
|
+
const merged = [];
|
|
870
|
+
let current = ordered[0];
|
|
871
|
+
for (const entry of ordered.slice(1)) {
|
|
872
|
+
if (selectionIntersects(current.selection, entry.selection)) {
|
|
873
|
+
const latest = entry.index > current.index ? entry : current;
|
|
874
|
+
const start = comparePosition(entry.selection.start, current.selection.start) < 0 ? entry.selection.start : current.selection.start;
|
|
875
|
+
const end = comparePosition(entry.selection.end, current.selection.end) > 0 ? entry.selection.end : current.selection.end;
|
|
876
|
+
let direction = latest.selection.direction;
|
|
877
|
+
if (direction === 0 && comparePosition(start, end) !== 0) direction = comparePosition(latest.selection.start, start) === 0 ? -1 : 1;
|
|
878
|
+
current = {
|
|
879
|
+
index: latest.index,
|
|
880
|
+
selection: {
|
|
881
|
+
direction,
|
|
882
|
+
end,
|
|
883
|
+
start
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
merged.push(current);
|
|
889
|
+
current = entry;
|
|
890
|
+
}
|
|
891
|
+
merged.push(current);
|
|
892
|
+
return merged.sort((a, b) => a.index - b.index).map(({ selection }) => selection);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Converts selections into merged line blocks for line-based commands.
|
|
896
|
+
*/
|
|
897
|
+
function getSelectedLineBlocks(selections) {
|
|
898
|
+
const blocks = selections.map((selection) => {
|
|
899
|
+
let endLine = selection.end.line;
|
|
900
|
+
if (selection.end.character === 0 && comparePosition(selection.start, selection.end) !== 0) endLine--;
|
|
901
|
+
return {
|
|
902
|
+
startLine: selection.start.line,
|
|
903
|
+
endLine: Math.max(selection.start.line, endLine)
|
|
904
|
+
};
|
|
905
|
+
}).sort((a, b) => {
|
|
906
|
+
const startOrder = a.startLine - b.startLine;
|
|
907
|
+
return startOrder !== 0 ? startOrder : a.endLine - b.endLine;
|
|
908
|
+
});
|
|
909
|
+
const merged = [];
|
|
910
|
+
for (const block of blocks) {
|
|
911
|
+
const previous = merged.at(-1);
|
|
912
|
+
if (previous !== void 0 && block.startLine <= previous.endLine + 1) previous.endLine = Math.max(previous.endLine, block.endLine);
|
|
913
|
+
else merged.push({ ...block });
|
|
914
|
+
}
|
|
915
|
+
return merged;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Moves a selection's line positions after its lines are shifted, clamping to
|
|
919
|
+
* the target document bounds.
|
|
920
|
+
*/
|
|
921
|
+
function shiftSelectionLines(selection, direction, lineCount, getLineLength) {
|
|
922
|
+
const shiftPosition = (position) => {
|
|
923
|
+
const line = position.line + direction;
|
|
924
|
+
if (line >= lineCount) {
|
|
925
|
+
const lastLine = Math.max(0, lineCount - 1);
|
|
926
|
+
return {
|
|
927
|
+
line: lastLine,
|
|
928
|
+
character: getLineLength(lastLine)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
if (line < 0) return {
|
|
932
|
+
line: 0,
|
|
933
|
+
character: 0
|
|
934
|
+
};
|
|
935
|
+
return {
|
|
936
|
+
line,
|
|
937
|
+
character: position.character
|
|
938
|
+
};
|
|
939
|
+
};
|
|
940
|
+
return {
|
|
941
|
+
start: shiftPosition(selection.start),
|
|
942
|
+
end: shiftPosition(selection.end),
|
|
943
|
+
direction: selection.direction
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Finds the next matching word and updates the selections.
|
|
948
|
+
*/
|
|
949
|
+
function findNexMatch(textDocument, selections) {
|
|
950
|
+
if (selections.length === 0) return;
|
|
951
|
+
const normalizedSelections = selections.map((selection) => isCollapsedSelection(selection) ? expandCollapsedSelectionToWord(textDocument, selection) : selection);
|
|
952
|
+
const texts = normalizedSelections.map((s) => textDocument.getText(s));
|
|
953
|
+
const needle = texts[0];
|
|
954
|
+
if (needle.length === 0 || texts.some((t) => t !== needle)) return;
|
|
955
|
+
const occupied = normalizedSelections.map((s) => [textDocument.offsetAt(s.start), textDocument.offsetAt(s.end)]);
|
|
956
|
+
const nextOffset = textDocument.findNextNonOverlappingSubstring(needle, occupied);
|
|
957
|
+
if (nextOffset === void 0) return normalizedSelections.some((selection, index) => {
|
|
958
|
+
const original = selections[index];
|
|
959
|
+
return comparePosition(selection.start, original.start) !== 0 || comparePosition(selection.end, original.end) !== 0 || selection.direction !== original.direction;
|
|
960
|
+
}) ? normalizedSelections : void 0;
|
|
961
|
+
const added = createSelectionFromAnchorAndFocusOffsets(textDocument, nextOffset, nextOffset + needle.length);
|
|
962
|
+
return [...normalizedSelections, added];
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Get the full selection of the document.
|
|
966
|
+
*/
|
|
967
|
+
function getDocumentFullSelection(textDocument) {
|
|
968
|
+
const lastLine = textDocument.lineCount - 1;
|
|
969
|
+
return {
|
|
970
|
+
start: {
|
|
971
|
+
line: 0,
|
|
972
|
+
character: 0
|
|
973
|
+
},
|
|
974
|
+
end: {
|
|
975
|
+
line: lastLine,
|
|
976
|
+
character: textDocument.getLineLength(lastLine)
|
|
977
|
+
},
|
|
978
|
+
direction: 1
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Get the boundary selection of the document.
|
|
983
|
+
*/
|
|
984
|
+
function getDocumentBoundarySelection(textDocument, atEnd, trimmedEndNewLine) {
|
|
985
|
+
let line = 0;
|
|
986
|
+
if (atEnd) {
|
|
987
|
+
const lastLine = textDocument.lineCount - 1;
|
|
988
|
+
line = trimmedEndNewLine === true && lastLine > 0 && textDocument.getLineLength(lastLine) === 0 ? lastLine - 1 : lastLine;
|
|
989
|
+
}
|
|
990
|
+
const character = atEnd ? textDocument.getLineLength(line) : 0;
|
|
991
|
+
const start = {
|
|
992
|
+
line,
|
|
993
|
+
character
|
|
994
|
+
};
|
|
995
|
+
return {
|
|
996
|
+
start,
|
|
997
|
+
end: start,
|
|
998
|
+
direction: 1
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
/** Resolves the document offset range one selection contributes to a copy. */
|
|
1002
|
+
function resolveClipboardRegion(textDocument, selection) {
|
|
1003
|
+
if (isCollapsedSelection(selection)) {
|
|
1004
|
+
const line = selection.start.line;
|
|
1005
|
+
return {
|
|
1006
|
+
start: textDocument.offsetAt({
|
|
1007
|
+
line,
|
|
1008
|
+
character: 0
|
|
1009
|
+
}),
|
|
1010
|
+
end: line < textDocument.lineCount - 1 ? textDocument.offsetAt({
|
|
1011
|
+
line: line + 1,
|
|
1012
|
+
character: 0
|
|
1013
|
+
}) : textDocument.offsetAt({
|
|
1014
|
+
line,
|
|
1015
|
+
character: textDocument.getLineLength(line)
|
|
1016
|
+
})
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
const start = textDocument.offsetAt(selection.start);
|
|
1020
|
+
const end = textDocument.offsetAt(selection.end);
|
|
1021
|
+
return start <= end ? {
|
|
1022
|
+
start,
|
|
1023
|
+
end
|
|
1024
|
+
} : {
|
|
1025
|
+
start: end,
|
|
1026
|
+
end: start
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Resolves the document offset range each selection contributes to the
|
|
1031
|
+
* clipboard, ordered by position. A collapsed selection contributes its whole
|
|
1032
|
+
* logical line including the trailing line break; the final line has no
|
|
1033
|
+
* trailing break to include. A ranged selection contributes the selected text.
|
|
1034
|
+
*/
|
|
1035
|
+
function resolveClipboardRegions(textDocument, selections) {
|
|
1036
|
+
return selections.map((selection) => resolveClipboardRegion(textDocument, selection)).sort((a, b) => {
|
|
1037
|
+
const startOrder = a.start - b.start;
|
|
1038
|
+
return startOrder !== 0 ? startOrder : a.end - b.end;
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Gets the text contributed by each selection in document order, preserving
|
|
1043
|
+
* the pairing needed to paste the values into another set of selections.
|
|
1044
|
+
*/
|
|
1045
|
+
function getSelectionClipboardTexts(textDocument, selections) {
|
|
1046
|
+
return resolveClipboardRegions(textDocument, selections).map(({ start, end }) => textDocument.getTextSlice(start, end));
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Get the clipboard text of the selections for the given text document. Used by
|
|
1050
|
+
* both copy and cut so the two stay in sync. Overlapping regions (e.g. several
|
|
1051
|
+
* carets on one line) are merged so the same text is never emitted twice, and a
|
|
1052
|
+
* line-ending separator is inserted only between regions that aren't already
|
|
1053
|
+
* contiguous in the document.
|
|
1054
|
+
*/
|
|
1055
|
+
function getSelectionText(textDocument, selections) {
|
|
1056
|
+
const regions = resolveClipboardRegions(textDocument, selections);
|
|
1057
|
+
const eol = textDocument.eol;
|
|
1058
|
+
let result = "";
|
|
1059
|
+
let prevEnd = -1;
|
|
1060
|
+
for (const region of regions) {
|
|
1061
|
+
if (region.end <= region.start) continue;
|
|
1062
|
+
if (region.start <= prevEnd) {
|
|
1063
|
+
if (region.end > prevEnd) {
|
|
1064
|
+
result += textDocument.getTextSlice(prevEnd, region.end);
|
|
1065
|
+
prevEnd = region.end;
|
|
1066
|
+
}
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (result.length > 0 && !endsWithLineBreak(result)) result += eol;
|
|
1070
|
+
result += textDocument.getTextSlice(region.start, region.end);
|
|
1071
|
+
prevEnd = region.end;
|
|
1072
|
+
}
|
|
1073
|
+
return result;
|
|
1074
|
+
}
|
|
1075
|
+
function resolveSelectionCutEdit(textDocument, selection) {
|
|
1076
|
+
if (isCollapsedSelection(selection)) return resolveCollapsedSelectionCutEdit(textDocument, selection);
|
|
1077
|
+
const [start, end] = comparePosition(selection.start, selection.end) <= 0 ? [selection.start, selection.end] : [selection.end, selection.start];
|
|
1078
|
+
return {
|
|
1079
|
+
start: textDocument.offsetAt(start),
|
|
1080
|
+
end: textDocument.offsetAt(end),
|
|
1081
|
+
text: ""
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function resolveCollapsedSelectionCutEdit(textDocument, selection) {
|
|
1085
|
+
const line = selection.start.line;
|
|
1086
|
+
const lineStart = textDocument.offsetAt({
|
|
1087
|
+
line,
|
|
1088
|
+
character: 0
|
|
1089
|
+
});
|
|
1090
|
+
const lineEnd = textDocument.offsetAt({
|
|
1091
|
+
line,
|
|
1092
|
+
character: textDocument.getLineLength(line)
|
|
1093
|
+
});
|
|
1094
|
+
if (line < textDocument.lineCount - 1) return {
|
|
1095
|
+
start: lineStart,
|
|
1096
|
+
end: textDocument.offsetAt({
|
|
1097
|
+
line: line + 1,
|
|
1098
|
+
character: 0
|
|
1099
|
+
}),
|
|
1100
|
+
text: ""
|
|
1101
|
+
};
|
|
1102
|
+
if (line > 0) return {
|
|
1103
|
+
start: textDocument.offsetAt({
|
|
1104
|
+
line: line - 1,
|
|
1105
|
+
character: textDocument.getLineLength(line - 1)
|
|
1106
|
+
}),
|
|
1107
|
+
end: lineEnd,
|
|
1108
|
+
text: ""
|
|
1109
|
+
};
|
|
1110
|
+
return {
|
|
1111
|
+
start: lineStart,
|
|
1112
|
+
end: lineEnd,
|
|
1113
|
+
text: ""
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function mergeCutEdits(orderedCuts) {
|
|
1117
|
+
const edits = [];
|
|
1118
|
+
for (const { edit } of orderedCuts) {
|
|
1119
|
+
if (edit.start >= edit.end) continue;
|
|
1120
|
+
const last = edits.at(-1);
|
|
1121
|
+
if (last !== void 0 && edit.start <= last.end) edits[edits.length - 1] = {
|
|
1122
|
+
start: last.start,
|
|
1123
|
+
end: Math.max(last.end, edit.end),
|
|
1124
|
+
text: ""
|
|
1125
|
+
};
|
|
1126
|
+
else edits.push(edit);
|
|
1127
|
+
}
|
|
1128
|
+
return edits;
|
|
1129
|
+
}
|
|
1130
|
+
function mapCutSelectionOffsets(orderedCuts, edits) {
|
|
1131
|
+
const nextOffsets = Array.from({ length: orderedCuts.length });
|
|
1132
|
+
let editIndex = 0;
|
|
1133
|
+
let offsetDelta = 0;
|
|
1134
|
+
for (const cut of orderedCuts) {
|
|
1135
|
+
while (editIndex < edits.length && cut.edit.start > edits[editIndex].end) {
|
|
1136
|
+
const edit = edits[editIndex];
|
|
1137
|
+
offsetDelta -= edit.end - edit.start;
|
|
1138
|
+
editIndex++;
|
|
1139
|
+
}
|
|
1140
|
+
const edit = edits[editIndex];
|
|
1141
|
+
if (edit !== void 0 && cut.edit.start >= edit.start && cut.edit.start <= edit.end) nextOffsets[cut.index] = edit.start + offsetDelta;
|
|
1142
|
+
else nextOffsets[cut.index] = cut.edit.start + offsetDelta;
|
|
1143
|
+
}
|
|
1144
|
+
return nextOffsets;
|
|
1145
|
+
}
|
|
1146
|
+
function resolveSelectionCut(textDocument, selections) {
|
|
1147
|
+
const orderedCuts = [...selections.map((selection, index) => ({
|
|
1148
|
+
index,
|
|
1149
|
+
edit: resolveSelectionCutEdit(textDocument, selection)
|
|
1150
|
+
}))].sort((a, b) => {
|
|
1151
|
+
const startOrder = a.edit.start - b.edit.start;
|
|
1152
|
+
if (startOrder !== 0) return startOrder;
|
|
1153
|
+
const endOrder = a.edit.end - b.edit.end;
|
|
1154
|
+
if (endOrder !== 0) return endOrder;
|
|
1155
|
+
return a.index - b.index;
|
|
1156
|
+
});
|
|
1157
|
+
const edits = mergeCutEdits(orderedCuts);
|
|
1158
|
+
return {
|
|
1159
|
+
text: getSelectionText(textDocument, selections),
|
|
1160
|
+
edits,
|
|
1161
|
+
nextSelectionOffsets: mapCutSelectionOffsets(orderedCuts, edits)
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Get the anchor node and offset for a selection.
|
|
1166
|
+
*/
|
|
1167
|
+
function getSelectionAnchor(lineElement, character) {
|
|
1168
|
+
const ch = Math.max(0, character);
|
|
1169
|
+
const tokens = collectTokens(lineElement);
|
|
1170
|
+
let last = null;
|
|
1171
|
+
for (const token of tokens) {
|
|
1172
|
+
last = token;
|
|
1173
|
+
const base = getCharacterIndex(token);
|
|
1174
|
+
if (ch <= base + (token.textContent?.length ?? 0)) {
|
|
1175
|
+
const anchor = textAt(token, ch < base ? 0 : ch - base);
|
|
1176
|
+
if (anchor !== null) return anchor;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (last !== null) {
|
|
1180
|
+
const anchor = textAt(last, last.textContent?.length ?? 0);
|
|
1181
|
+
if (anchor !== null) return anchor;
|
|
1182
|
+
return [last, 0];
|
|
1183
|
+
}
|
|
1184
|
+
let textOffset = 0;
|
|
1185
|
+
let lastTextNode = null;
|
|
1186
|
+
for (const child of lineElement.childNodes) {
|
|
1187
|
+
if (child.nodeType === 1 && child.tagName === "BR") return [child, 0];
|
|
1188
|
+
if (child.nodeType !== 3) continue;
|
|
1189
|
+
lastTextNode = child;
|
|
1190
|
+
const len = getTextOffset(lastTextNode.textContent, lastTextNode.textContent?.length ?? 0);
|
|
1191
|
+
if (ch <= textOffset + len) return [lastTextNode, getTextOffset(lastTextNode.textContent, ch - textOffset)];
|
|
1192
|
+
textOffset += len;
|
|
1193
|
+
}
|
|
1194
|
+
if (lastTextNode !== null) return [lastTextNode, getTextOffset(lastTextNode.textContent, lastTextNode.textContent?.length ?? 0)];
|
|
1195
|
+
return [lineElement, 0];
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Expands a zero-width selection to the word-like segment that contains the caret.
|
|
1199
|
+
*/
|
|
1200
|
+
function expandCollapsedSelectionToWord(textDocument, selection) {
|
|
1201
|
+
const { line, character } = selection.start;
|
|
1202
|
+
const lineText = textDocument.getLineText(line);
|
|
1203
|
+
const span = expandCollapsedLineWord(lineText, Math.max(0, Math.min(character, lineText.length)));
|
|
1204
|
+
if (span === void 0) return selection;
|
|
1205
|
+
return {
|
|
1206
|
+
start: {
|
|
1207
|
+
line,
|
|
1208
|
+
character: span.start
|
|
1209
|
+
},
|
|
1210
|
+
end: {
|
|
1211
|
+
line,
|
|
1212
|
+
character: span.end
|
|
1213
|
+
},
|
|
1214
|
+
direction: 1
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function expandCollapsedLineWord(lineText, character) {
|
|
1218
|
+
const segmenter = createSegmenter({ granularity: "word" });
|
|
1219
|
+
if (segmenter !== void 0) {
|
|
1220
|
+
for (const seg of segmenter.segment(lineText)) {
|
|
1221
|
+
if (seg.isWordLike !== true) continue;
|
|
1222
|
+
const lo = seg.index;
|
|
1223
|
+
const hi = lo + seg.segment.length;
|
|
1224
|
+
if (character >= lo && character <= hi) return {
|
|
1225
|
+
start: lo,
|
|
1226
|
+
end: hi
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const wordRe = /[\p{Alphabetic}\p{Number}_]+/gu;
|
|
1232
|
+
let match;
|
|
1233
|
+
while ((match = wordRe.exec(lineText)) !== null) {
|
|
1234
|
+
const lo = match.index;
|
|
1235
|
+
const hi = lo + match[0].length;
|
|
1236
|
+
if (character >= lo && character <= hi) return {
|
|
1237
|
+
start: lo,
|
|
1238
|
+
end: hi
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
function resolveDeleteWordBackwardRange(textDocument, selection) {
|
|
1243
|
+
if (!isCollapsedSelection(selection)) return [selection.start, selection.end];
|
|
1244
|
+
const caret = getCaretPosition(selection);
|
|
1245
|
+
const { line, character: head } = caret;
|
|
1246
|
+
if (head === 0) {
|
|
1247
|
+
if (line === 0) return [caret, caret];
|
|
1248
|
+
const prevLineLength = textDocument.getLineLength(line - 1);
|
|
1249
|
+
return [{
|
|
1250
|
+
line: line - 1,
|
|
1251
|
+
character: prevLineLength
|
|
1252
|
+
}, {
|
|
1253
|
+
line,
|
|
1254
|
+
character: 0
|
|
1255
|
+
}];
|
|
1256
|
+
}
|
|
1257
|
+
const lineText = textDocument.getLineText(line);
|
|
1258
|
+
const graphemeStarts = getLineGraphemeStarts(lineText);
|
|
1259
|
+
let pos = head;
|
|
1260
|
+
let match;
|
|
1261
|
+
while (pos > 0) {
|
|
1262
|
+
const prev = findClusterBreak(lineText, pos, false, graphemeStarts);
|
|
1263
|
+
const nextChar = lineText.slice(prev, pos);
|
|
1264
|
+
const nextMatch = !/\S/.test(nextChar) ? 0 : /\p{Alphabetic}|\p{Number}|_/u.test(nextChar) ? 1 : 2;
|
|
1265
|
+
if (match !== void 0 && nextMatch !== match) break;
|
|
1266
|
+
if (nextMatch !== 0 || pos !== head) match = nextMatch;
|
|
1267
|
+
pos = prev;
|
|
1268
|
+
}
|
|
1269
|
+
return [{
|
|
1270
|
+
line,
|
|
1271
|
+
character: pos
|
|
1272
|
+
}, {
|
|
1273
|
+
line,
|
|
1274
|
+
character: head
|
|
1275
|
+
}];
|
|
1276
|
+
}
|
|
1277
|
+
function findClusterBreak(text, pos, forward, graphemeStarts) {
|
|
1278
|
+
if (forward) {
|
|
1279
|
+
for (const start of graphemeStarts) if (start > pos) return start;
|
|
1280
|
+
return text.length;
|
|
1281
|
+
}
|
|
1282
|
+
for (let i = graphemeStarts.length - 1; i >= 0; i--) {
|
|
1283
|
+
const start = graphemeStarts[i];
|
|
1284
|
+
if (start < pos) return start;
|
|
1285
|
+
}
|
|
1286
|
+
return 0;
|
|
1287
|
+
}
|
|
1288
|
+
function getLineGraphemeStarts(lineText) {
|
|
1289
|
+
const graphemeStarts = [0];
|
|
1290
|
+
const segmenter = getGraphemeSegmenter();
|
|
1291
|
+
if (segmenter !== void 0) {
|
|
1292
|
+
for (const segment of segmenter.segment(lineText)) if (segment.index > 0) graphemeStarts.push(segment.index);
|
|
1293
|
+
return graphemeStarts;
|
|
1294
|
+
}
|
|
1295
|
+
let index = 0;
|
|
1296
|
+
for (const codePoint of lineText) {
|
|
1297
|
+
if (index > 0) graphemeStarts.push(index);
|
|
1298
|
+
index += codePoint.length;
|
|
1299
|
+
}
|
|
1300
|
+
return graphemeStarts;
|
|
1301
|
+
}
|
|
1302
|
+
function stepCharacterByGrapheme(textDocument, line, character, forward) {
|
|
1303
|
+
const lineLength = textDocument.getLineLength(line);
|
|
1304
|
+
if (forward) {
|
|
1305
|
+
if (character >= lineLength) return lineLength;
|
|
1306
|
+
const lineStart = textDocument.offsetAt({
|
|
1307
|
+
line,
|
|
1308
|
+
character: 0
|
|
1309
|
+
});
|
|
1310
|
+
const suffix = textDocument.getTextSlice(lineStart + character, lineStart + lineLength);
|
|
1311
|
+
const segmenter = getGraphemeSegmenter();
|
|
1312
|
+
if (segmenter !== void 0) {
|
|
1313
|
+
for (const segment of segmenter.segment(suffix)) return character + segment.segment.length;
|
|
1314
|
+
return lineLength;
|
|
1315
|
+
}
|
|
1316
|
+
for (const codePoint of suffix) return character + codePoint.length;
|
|
1317
|
+
return lineLength;
|
|
1318
|
+
}
|
|
1319
|
+
if (character <= 0) return 0;
|
|
1320
|
+
const lineStart = textDocument.offsetAt({
|
|
1321
|
+
line,
|
|
1322
|
+
character: 0
|
|
1323
|
+
});
|
|
1324
|
+
const prefix = textDocument.getTextSlice(lineStart, lineStart + character);
|
|
1325
|
+
let prevStart = 0;
|
|
1326
|
+
const segmenter = getGraphemeSegmenter();
|
|
1327
|
+
if (segmenter !== void 0) {
|
|
1328
|
+
for (const segment of segmenter.segment(prefix)) prevStart = segment.index;
|
|
1329
|
+
return prevStart;
|
|
1330
|
+
}
|
|
1331
|
+
let index = 0;
|
|
1332
|
+
for (const codePoint of prefix) {
|
|
1333
|
+
prevStart = index;
|
|
1334
|
+
index += codePoint.length;
|
|
1335
|
+
}
|
|
1336
|
+
return prevStart;
|
|
1337
|
+
}
|
|
1338
|
+
function getSelectionAnchorAndFocusOffsets(textDocument, selection) {
|
|
1339
|
+
const isBackward = selection.direction === -1;
|
|
1340
|
+
return [textDocument.offsetAt(isBackward ? selection.end : selection.start), textDocument.offsetAt(getCaretPosition(selection))];
|
|
1341
|
+
}
|
|
1342
|
+
function resolveDeleteHardLineForwardRange(textDocument, selection) {
|
|
1343
|
+
if (!isCollapsedSelection(selection)) return {
|
|
1344
|
+
start: selection.start,
|
|
1345
|
+
end: selection.end
|
|
1346
|
+
};
|
|
1347
|
+
const { line, character } = selection.start;
|
|
1348
|
+
const lineLength = textDocument.getLineText(line).length;
|
|
1349
|
+
if (character < lineLength) return {
|
|
1350
|
+
start: {
|
|
1351
|
+
line,
|
|
1352
|
+
character
|
|
1353
|
+
},
|
|
1354
|
+
end: {
|
|
1355
|
+
line,
|
|
1356
|
+
character: lineLength
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
if (line < textDocument.lineCount - 1) return {
|
|
1360
|
+
start: {
|
|
1361
|
+
line,
|
|
1362
|
+
character
|
|
1363
|
+
},
|
|
1364
|
+
end: {
|
|
1365
|
+
line: line + 1,
|
|
1366
|
+
character: 0
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
return {
|
|
1370
|
+
start: {
|
|
1371
|
+
line,
|
|
1372
|
+
character
|
|
1373
|
+
},
|
|
1374
|
+
end: {
|
|
1375
|
+
line,
|
|
1376
|
+
character
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function expandSingleNewlineInsert(textDocument, insertText, insertStartOffset) {
|
|
1381
|
+
if (insertText !== "\n" && insertText !== "\r" && insertText !== "\r\n") return insertText;
|
|
1382
|
+
const line = textDocument.positionAt(insertStartOffset).line;
|
|
1383
|
+
const lineText = textDocument.getLineText(line);
|
|
1384
|
+
const indentLen = getLeadingSpaces(lineText);
|
|
1385
|
+
if (indentLen === 0) return insertText;
|
|
1386
|
+
return insertText + lineText.slice(0, indentLen);
|
|
1387
|
+
}
|
|
1388
|
+
function getLeadingSpaces(text) {
|
|
1389
|
+
let indent = 0;
|
|
1390
|
+
for (; indent < text.length; indent++) {
|
|
1391
|
+
const c = text.charCodeAt(indent);
|
|
1392
|
+
if (c !== 32 && c !== 9) break;
|
|
1393
|
+
}
|
|
1394
|
+
return indent;
|
|
1395
|
+
}
|
|
1396
|
+
function createSelectionsFromOffsetPairs(textDocument, offsetPairs) {
|
|
1397
|
+
const normalizedOffsets = [];
|
|
1398
|
+
for (const [anchorOffset, focusOffset] of offsetPairs) normalizedOffsets.push(Math.min(anchorOffset, focusOffset), Math.max(anchorOffset, focusOffset));
|
|
1399
|
+
const positions = textDocument.positionsAt(normalizedOffsets);
|
|
1400
|
+
return offsetPairs.map(([anchorOffset, focusOffset], index) => {
|
|
1401
|
+
const direction = anchorOffset === focusOffset ? 0 : anchorOffset < focusOffset ? 1 : -1;
|
|
1402
|
+
return {
|
|
1403
|
+
start: positions[index * 2],
|
|
1404
|
+
end: positions[index * 2 + 1],
|
|
1405
|
+
direction
|
|
1406
|
+
};
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
function normalizeLeadingIndentForChange(textDocument, change, tabSize) {
|
|
1410
|
+
if (change.text !== "" || change.start !== change.end - 1) return change;
|
|
1411
|
+
const caretPosition = textDocument.positionAt(change.end);
|
|
1412
|
+
if (caretPosition.character === 0) return change;
|
|
1413
|
+
const lineText = textDocument.getLineText(caretPosition.line);
|
|
1414
|
+
const leadingText = lineText.slice(0, caretPosition.character);
|
|
1415
|
+
if (/[^ \t]/.test(leadingText)) return change;
|
|
1416
|
+
if (lineText[caretPosition.character - 1] === " ") return change;
|
|
1417
|
+
const softTabStart = Math.max(0, caretPosition.character - tabSize);
|
|
1418
|
+
const softTabText = lineText.slice(softTabStart, caretPosition.character);
|
|
1419
|
+
if (softTabText.length === tabSize && /^ +$/.test(softTabText)) return {
|
|
1420
|
+
...change,
|
|
1421
|
+
start: change.end - softTabText.length
|
|
1422
|
+
};
|
|
1423
|
+
return change;
|
|
1424
|
+
}
|
|
1425
|
+
function boundaryToPosition(node, offset) {
|
|
1426
|
+
let lineEl = node.nodeType === 1 ? node : node.parentElement;
|
|
1427
|
+
while (lineEl !== null && getLineIndex(lineEl) === void 0) lineEl = lineEl.parentElement;
|
|
1428
|
+
if (lineEl === null) return null;
|
|
1429
|
+
const line = getLineIndex(lineEl);
|
|
1430
|
+
if (line === void 0) return null;
|
|
1431
|
+
if (node.nodeType === 3) {
|
|
1432
|
+
if (node.parentElement === null) return null;
|
|
1433
|
+
if (findTokenSpan(node.parentElement) !== null) return {
|
|
1434
|
+
line,
|
|
1435
|
+
character: getLineChildEnd(node, offset)
|
|
1436
|
+
};
|
|
1437
|
+
return {
|
|
1438
|
+
line,
|
|
1439
|
+
character: offsetBefore(lineEl, node) + getTextOffset(node.textContent, offset)
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
if (node.nodeType === 1) {
|
|
1443
|
+
const el = node;
|
|
1444
|
+
if (el.tagName === "DIV") {
|
|
1445
|
+
let character = 0;
|
|
1446
|
+
for (let i = 0; i < offset; i++) character = getLineChildEnd(el.childNodes[i]);
|
|
1447
|
+
return {
|
|
1448
|
+
line,
|
|
1449
|
+
character
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
if (el.tagName === "BR") return {
|
|
1453
|
+
line,
|
|
1454
|
+
character: 0
|
|
1455
|
+
};
|
|
1456
|
+
if (el.tagName === "SPAN") {
|
|
1457
|
+
if (offset < el.childNodes.length) {
|
|
1458
|
+
const next = el.childNodes[offset];
|
|
1459
|
+
if (next?.nodeType === 1) {
|
|
1460
|
+
const nextBase = getCharacterIndex(next);
|
|
1461
|
+
if (nextBase !== void 0) return {
|
|
1462
|
+
line,
|
|
1463
|
+
character: nextBase
|
|
1464
|
+
};
|
|
1465
|
+
const token = findTokenSpan(next);
|
|
1466
|
+
const tokenBase = token === null ? void 0 : getCharacterIndex(token);
|
|
1467
|
+
if (tokenBase !== void 0) return {
|
|
1468
|
+
line,
|
|
1469
|
+
character: tokenBase
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
line,
|
|
1475
|
+
character: offset > 0 ? getLineChildEnd(el.childNodes[offset - 1]) : offsetBefore(lineEl, el)
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
line,
|
|
1480
|
+
character: offsetBefore(lineEl, el)
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
function collectTokens(line) {
|
|
1486
|
+
const tokens = [];
|
|
1487
|
+
for (const child of line.childNodes) {
|
|
1488
|
+
if (child.nodeType !== 1) continue;
|
|
1489
|
+
const el = child;
|
|
1490
|
+
if (el.tagName !== "SPAN") continue;
|
|
1491
|
+
if (getCharacterIndex(el) !== void 0) {
|
|
1492
|
+
tokens.push(el);
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
for (const nested of el.childNodes) if (nested.nodeType === 1 && getCharacterIndex(nested) !== void 0) tokens.push(nested);
|
|
1496
|
+
}
|
|
1497
|
+
return tokens;
|
|
1498
|
+
}
|
|
1499
|
+
function textAt(token, offset) {
|
|
1500
|
+
let remaining = Math.max(0, offset);
|
|
1501
|
+
const stack = [{
|
|
1502
|
+
container: token,
|
|
1503
|
+
index: 0
|
|
1504
|
+
}];
|
|
1505
|
+
while (stack.length > 0) {
|
|
1506
|
+
const frame = stack[stack.length - 1];
|
|
1507
|
+
if (frame.index >= frame.container.childNodes.length) {
|
|
1508
|
+
stack.pop();
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const walkNode = frame.container.childNodes[frame.index];
|
|
1512
|
+
frame.index++;
|
|
1513
|
+
if (walkNode.nodeType === 3) {
|
|
1514
|
+
const len = getTextOffset(walkNode.textContent, walkNode.textContent?.length ?? 0);
|
|
1515
|
+
if (remaining <= len) return [walkNode, remaining];
|
|
1516
|
+
remaining -= len;
|
|
1517
|
+
} else if (walkNode.nodeType === 1) stack.push({
|
|
1518
|
+
container: walkNode,
|
|
1519
|
+
index: 0
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
function textLengthBefore(root, target) {
|
|
1525
|
+
let before = 0;
|
|
1526
|
+
const stack = [{
|
|
1527
|
+
container: root,
|
|
1528
|
+
index: 0
|
|
1529
|
+
}];
|
|
1530
|
+
while (stack.length > 0) {
|
|
1531
|
+
const frame = stack[stack.length - 1];
|
|
1532
|
+
if (frame.index >= frame.container.childNodes.length) {
|
|
1533
|
+
stack.pop();
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
const walkNode = frame.container.childNodes[frame.index];
|
|
1537
|
+
if (walkNode === target) return before;
|
|
1538
|
+
frame.index++;
|
|
1539
|
+
if (walkNode.nodeType === 3) before += getTextOffset(walkNode.textContent, walkNode.textContent?.length ?? 0);
|
|
1540
|
+
else if (walkNode.nodeType === 1) stack.push({
|
|
1541
|
+
container: walkNode,
|
|
1542
|
+
index: 0
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
return before;
|
|
1546
|
+
}
|
|
1547
|
+
function isInside(token, node) {
|
|
1548
|
+
let current = node;
|
|
1549
|
+
while (current !== null) {
|
|
1550
|
+
if (current === token) return true;
|
|
1551
|
+
current = current.parentElement;
|
|
1552
|
+
}
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1555
|
+
function offsetBefore(line, node) {
|
|
1556
|
+
if (node.parentElement === line) {
|
|
1557
|
+
let offset = 0;
|
|
1558
|
+
const index = Array.prototype.indexOf.call(line.childNodes, node);
|
|
1559
|
+
for (let i = 0; i < index; i++) offset = getLineChildEnd(line.childNodes[i]);
|
|
1560
|
+
return offset;
|
|
1561
|
+
}
|
|
1562
|
+
for (const token of collectTokens(line)) if (isInside(token, node)) return getCharacterIndex(token) + (node.nodeType === 3 ? textLengthBefore(token, node) : 0);
|
|
1563
|
+
let offset = 0;
|
|
1564
|
+
let target = node.nodeType === 1 ? node : node.parentElement;
|
|
1565
|
+
while (target !== null && target.parentElement !== null) {
|
|
1566
|
+
if (getLineIndex(target.parentElement) !== void 0) break;
|
|
1567
|
+
const parent = target.parentElement;
|
|
1568
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, target);
|
|
1569
|
+
for (let i = 0; i < index; i++) offset = getLineChildEnd(parent.childNodes[i]);
|
|
1570
|
+
target = parent;
|
|
1571
|
+
}
|
|
1572
|
+
return offset;
|
|
1573
|
+
}
|
|
1574
|
+
function findTokenSpan(el) {
|
|
1575
|
+
let current = el;
|
|
1576
|
+
while (current !== null) {
|
|
1577
|
+
if (getLineIndex(current) !== void 0) return null;
|
|
1578
|
+
if (getCharacterIndex(current) !== void 0) return current;
|
|
1579
|
+
current = current.parentElement;
|
|
1580
|
+
}
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
function getLineChildEnd(child, textOffsetInChild) {
|
|
1584
|
+
if (child === void 0) return 0;
|
|
1585
|
+
if (child.nodeType === 3) {
|
|
1586
|
+
const parent = child.parentElement;
|
|
1587
|
+
if (parent === null) return 0;
|
|
1588
|
+
const token = findTokenSpan(parent);
|
|
1589
|
+
if (token === null) return 0;
|
|
1590
|
+
const base = getCharacterIndex(token);
|
|
1591
|
+
if (base === void 0) return 0;
|
|
1592
|
+
const length = textOffsetInChild === void 0 ? getTextOffset(child.textContent, child.textContent?.length ?? 0) : getTextOffset(child.textContent, textOffsetInChild);
|
|
1593
|
+
return base + textLengthBefore(token, child) + length;
|
|
1594
|
+
}
|
|
1595
|
+
if (child.nodeType !== 1) return 0;
|
|
1596
|
+
const el = child;
|
|
1597
|
+
if (el.tagName !== "SPAN" && el.tagName !== "BR") return 0;
|
|
1598
|
+
const base = getCharacterIndex(el);
|
|
1599
|
+
if (base !== void 0) return base + (el.textContent?.length ?? 0);
|
|
1600
|
+
let end = 0;
|
|
1601
|
+
for (const token of el.childNodes) end = Math.max(end, getLineChildEnd(token));
|
|
1602
|
+
return end;
|
|
1603
|
+
}
|
|
1604
|
+
function getLineIndex(el) {
|
|
1605
|
+
const { line, lineType } = el.dataset;
|
|
1606
|
+
if (line !== void 0 && lineType !== "change-deletion") {
|
|
1607
|
+
const lineNumber = parseInt(line, 10);
|
|
1608
|
+
if (!Number.isNaN(lineNumber)) return lineNumber - 1;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function getCharacterIndex(el) {
|
|
1612
|
+
const { char } = el.dataset;
|
|
1613
|
+
if (char !== void 0) {
|
|
1614
|
+
const charIndex = parseInt(char, 10);
|
|
1615
|
+
if (!Number.isNaN(charIndex)) return charIndex;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function getTextOffset(text, offset) {
|
|
1619
|
+
const value = text ?? "";
|
|
1620
|
+
const lineBreakIndex = value.search(/[\r\n]/);
|
|
1621
|
+
return Math.min(offset, lineBreakIndex === -1 ? value.length : lineBreakIndex);
|
|
1622
|
+
}
|
|
1623
|
+
//#endregion
|
|
1624
|
+
export { DirectionBackward, DirectionForward, DirectionNone, applyDeleteCharacterToSelections, applyDeleteHardLineForwardToSelections, applyDeleteSoftLineBackwardToSelections, applyDeleteWordBackwardToSelections, applyTextChangeToSelections, applyTextReplaceToSelections, applyTransposeToSelections, comparePosition, convertSelection, createSelectionFrom, createSelectionFromAnchorAndFocusOffsets, expandCollapsedSelectionToWord, extendSelection, extendSelections, findNexMatch, getAutoSurroundReplacementTexts, getCaretPosition, getDocumentBoundarySelection, getDocumentFullSelection, getSelectedLineBlocks, getSelectionAnchor, getSelectionClipboardTexts, getSelectionText, isCollapsedSelection, isLineEditable, mapCursorMove, mapSelectionShift, mergeOverlappingSelections, remapSelectionsAfterEdits, resolveDeleteCharacterRange, resolveIndentEdits, resolveSelectionCut, selectionIntersects, shiftSelectionLines };
|
|
1625
|
+
|
|
1626
|
+
//# sourceMappingURL=selection.js.map
|