@pierre/diffs 1.3.0-beta.7 → 1.3.0-beta.9
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 +86 -4
- package/dist/components/CodeView.d.ts.map +1 -1
- package/dist/components/CodeView.js +248 -33
- package/dist/components/CodeView.js.map +1 -1
- package/dist/components/File.d.ts +1 -0
- package/dist/components/File.d.ts.map +1 -1
- package/dist/components/File.js +15 -4
- package/dist/components/File.js.map +1 -1
- package/dist/components/FileDiff.d.ts +2 -0
- package/dist/components/FileDiff.d.ts.map +1 -1
- package/dist/components/FileDiff.js +21 -6
- 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/VirtualizedFile.d.ts.map +1 -1
- package/dist/components/VirtualizedFile.js +3 -1
- package/dist/components/VirtualizedFile.js.map +1 -1
- package/dist/components/VirtualizedFileDiff.d.ts +3 -1
- package/dist/components/VirtualizedFileDiff.d.ts.map +1 -1
- package/dist/components/VirtualizedFileDiff.js +14 -6
- package/dist/components/VirtualizedFileDiff.js.map +1 -1
- package/dist/components/Virtualizer.d.ts +2 -0
- package/dist/components/Virtualizer.d.ts.map +1 -1
- package/dist/components/Virtualizer.js +18 -4
- package/dist/components/Virtualizer.js.map +1 -1
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/editor/command.d.ts +1 -1
- package/dist/editor/command.d.ts.map +1 -1
- package/dist/editor/command.js +8 -0
- package/dist/editor/command.js.map +1 -1
- package/dist/editor/editStack.d.ts.map +1 -1
- package/dist/editor/editStack.js +1 -1
- package/dist/editor/editStack.js.map +1 -1
- package/dist/editor/editor.d.ts +8 -1
- package/dist/editor/editor.d.ts.map +1 -1
- package/dist/editor/editor.js +188 -42
- package/dist/editor/editor.js.map +1 -1
- package/dist/editor/editor2.js +1 -1
- package/dist/editor/lineAnnotations.d.ts.map +1 -1
- package/dist/editor/lineAnnotations.js +84 -12
- package/dist/editor/lineAnnotations.js.map +1 -1
- package/dist/editor/marker.d.ts +5 -3
- package/dist/editor/marker.d.ts.map +1 -1
- package/dist/editor/marker.js +35 -12
- package/dist/editor/marker.js.map +1 -1
- package/dist/editor/matchBrackets.d.ts +8 -0
- package/dist/editor/matchBrackets.d.ts.map +1 -0
- package/dist/editor/matchBrackets.js +146 -0
- package/dist/editor/matchBrackets.js.map +1 -0
- package/dist/editor/popover.d.ts +45 -0
- package/dist/editor/popover.d.ts.map +1 -0
- package/dist/editor/popover.js +145 -0
- package/dist/editor/popover.js.map +1 -0
- package/dist/editor/searchPanel.d.ts +1 -0
- package/dist/editor/searchPanel.d.ts.map +1 -1
- package/dist/editor/searchPanel.js +6 -6
- package/dist/editor/searchPanel.js.map +1 -1
- package/dist/editor/selection.d.ts +13 -1
- package/dist/editor/selection.d.ts.map +1 -1
- package/dist/editor/selection.js +55 -4
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/selectionAction.d.ts +20 -3
- package/dist/editor/selectionAction.d.ts.map +1 -1
- package/dist/editor/selectionAction.js +41 -10
- package/dist/editor/selectionAction.js.map +1 -1
- package/dist/editor/textDocument.d.ts +2 -0
- package/dist/editor/textDocument.d.ts.map +1 -1
- package/dist/editor/textDocument.js +22 -6
- package/dist/editor/textDocument.js.map +1 -1
- package/dist/editor/tokenzier.d.ts +4 -7
- package/dist/editor/tokenzier.d.ts.map +1 -1
- package/dist/editor/tokenzier.js +76 -35
- package/dist/editor/tokenzier.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +5 -5
- 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 +25 -1
- package/dist/react/CodeView.d.ts.map +1 -1
- package/dist/react/CodeView.js +76 -17
- package/dist/react/CodeView.js.map +1 -1
- package/dist/react/EditorContext.d.ts +2 -2
- package/dist/react/EditorContext.d.ts.map +1 -1
- package/dist/react/EditorContext.js.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/renderers/DiffHunksRenderer.d.ts +1 -1
- package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
- package/dist/renderers/DiffHunksRenderer.js +24 -15
- package/dist/renderers/DiffHunksRenderer.js.map +1 -1
- package/dist/renderers/FileRenderer.d.ts +2 -1
- package/dist/renderers/FileRenderer.d.ts.map +1 -1
- package/dist/renderers/FileRenderer.js +58 -38
- package/dist/renderers/FileRenderer.js.map +1 -1
- package/dist/ssr/index.d.ts +2 -2
- package/dist/types.d.ts +42 -2
- 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/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/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/updateDiffHunks.d.ts.map +1 -1
- package/dist/utils/updateDiffHunks.js +23 -1
- package/dist/utils/updateDiffHunks.js.map +1 -1
- package/dist/worker/WorkerPoolManager.d.ts.map +1 -1
- package/dist/worker/WorkerPoolManager.js +4 -2
- package/dist/worker/WorkerPoolManager.js.map +1 -1
- package/dist/worker/types.d.ts +4 -0
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/worker-portable.js.map +1 -1
- package/dist/worker/worker.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matchBrackets.js","names":[],"sources":["../../src/editor/matchBrackets.ts"],"sourcesContent":["import type { Position, Range, TextDocument } from './textDocument';\nimport type { EditorTokenizer } from './tokenzier';\n\nconst OPEN_BRACKETS = new Map([\n ['(', ')'],\n ['[', ']'],\n ['{', '}'],\n]);\n\nconst CLOSE_BRACKETS = new Map(\n [...OPEN_BRACKETS].map(([open, close]) => [close, open])\n);\n\nconst MAX_BRACKET_SCAN_LINES = 1_000;\nconst MAX_BRACKET_SCAN_CHARACTERS = 50_000;\n\ninterface BracketPosition extends Position {\n char: string;\n}\n\nexport function findBracketMatchRanges<LAnnotation>(\n textDocument: TextDocument<LAnnotation>,\n tokenizer: EditorTokenizer,\n position: Position\n): [open: Range, close: Range] | undefined {\n const bracketPosition = findAdjacentBracket(\n textDocument,\n tokenizer,\n textDocument.normalizePosition(position)\n );\n if (bracketPosition === undefined) {\n return undefined;\n }\n\n const closingBracket = OPEN_BRACKETS.get(bracketPosition.char);\n const openingBracket = CLOSE_BRACKETS.get(bracketPosition.char);\n if (closingBracket !== undefined) {\n const matchPosition = findClosingBracket(\n textDocument,\n tokenizer,\n bracketPosition,\n closingBracket\n );\n return createBracketMatchRanges(bracketPosition, matchPosition);\n }\n if (openingBracket !== undefined) {\n const matchPosition = findOpeningBracket(\n textDocument,\n tokenizer,\n bracketPosition,\n openingBracket\n );\n return createBracketMatchRanges(matchPosition, bracketPosition);\n }\n return undefined;\n}\n\nfunction findAdjacentBracket<LAnnotation>(\n textDocument: TextDocument<LAnnotation>,\n tokenizer: EditorTokenizer,\n position: Position\n): BracketPosition | undefined {\n const previousPosition = getPreviousCharacterPosition(textDocument, position);\n if (previousPosition !== undefined) {\n const previousBracket = getBracketAtPosition(\n textDocument,\n tokenizer,\n previousPosition\n );\n if (previousBracket !== undefined) {\n return previousBracket;\n }\n }\n return getBracketAtPosition(textDocument, tokenizer, position);\n}\n\nfunction getPreviousCharacterPosition<LAnnotation>(\n textDocument: TextDocument<LAnnotation>,\n position: Position\n): Position | undefined {\n if (position.character > 0) {\n return { line: position.line, character: position.character - 1 };\n }\n if (position.line <= 0) {\n return undefined;\n }\n const previousLine = position.line - 1;\n const previousLineLength = textDocument.getLineText(previousLine).length;\n if (previousLineLength === 0) {\n return undefined;\n }\n return { line: previousLine, character: previousLineLength - 1 };\n}\n\nfunction getBracketAtPosition<LAnnotation>(\n textDocument: TextDocument<LAnnotation>,\n tokenizer: EditorTokenizer,\n position: Position\n): BracketPosition | undefined {\n const lineText = textDocument.getLineText(position.line);\n const char = lineText[position.character];\n if (\n char === undefined ||\n (!OPEN_BRACKETS.has(char) && !CLOSE_BRACKETS.has(char)) ||\n isInIgnoredTokenRange(tokenizer, position)\n ) {\n return undefined;\n }\n return { ...position, char };\n}\n\nfunction findClosingBracket<LAnnotation>(\n textDocument: TextDocument<LAnnotation>,\n tokenizer: EditorTokenizer,\n bracketPosition: BracketPosition,\n closingBracket: string\n): BracketPosition | undefined {\n let depth = 0;\n let scannedLines = 0;\n let scannedCharacters = 0;\n for (let line = bracketPosition.line; line < textDocument.lineCount; line++) {\n if (scannedLines >= MAX_BRACKET_SCAN_LINES) {\n return undefined;\n }\n scannedLines++;\n const lineText = textDocument.getLineText(line);\n const ignoredRanges = tokenizer.getStringCommentRegexpRangesInLine(line);\n const ignoredRangeCursor = { index: 0 };\n const startCharacter =\n line === bracketPosition.line ? bracketPosition.character : 0;\n for (\n let character = startCharacter;\n character < lineText.length;\n character++\n ) {\n if (scannedCharacters >= MAX_BRACKET_SCAN_CHARACTERS) {\n return undefined;\n }\n scannedCharacters++;\n if (\n isCharacterIgnoredForward(ignoredRanges, character, ignoredRangeCursor)\n ) {\n continue;\n }\n const char = lineText[character];\n if (char === bracketPosition.char) {\n depth++;\n } else if (char === closingBracket) {\n depth--;\n if (depth === 0) {\n return { line, character, char };\n }\n }\n }\n }\n return undefined;\n}\n\nfunction findOpeningBracket<LAnnotation>(\n textDocument: TextDocument<LAnnotation>,\n tokenizer: EditorTokenizer,\n bracketPosition: BracketPosition,\n openingBracket: string\n): BracketPosition | undefined {\n let depth = 0;\n let scannedLines = 0;\n let scannedCharacters = 0;\n for (let line = bracketPosition.line; line >= 0; line--) {\n if (scannedLines >= MAX_BRACKET_SCAN_LINES) {\n return undefined;\n }\n scannedLines++;\n const lineText = textDocument.getLineText(line);\n const ignoredRanges = tokenizer.getStringCommentRegexpRangesInLine(line);\n const ignoredRangeCursor = {\n index: ignoredRanges === null ? -1 : ignoredRanges.length - 1,\n };\n const startCharacter =\n line === bracketPosition.line\n ? bracketPosition.character\n : lineText.length - 1;\n for (let character = startCharacter; character >= 0; character--) {\n if (scannedCharacters >= MAX_BRACKET_SCAN_CHARACTERS) {\n return undefined;\n }\n scannedCharacters++;\n if (\n isCharacterIgnoredBackward(ignoredRanges, character, ignoredRangeCursor)\n ) {\n continue;\n }\n const char = lineText[character];\n if (char === bracketPosition.char) {\n depth++;\n } else if (char === openingBracket) {\n depth--;\n if (depth === 0) {\n return { line, character, char };\n }\n }\n }\n }\n return undefined;\n}\n\nfunction isInIgnoredTokenRange(\n tokenizer: EditorTokenizer,\n position: Position\n): boolean {\n const ranges = tokenizer.getStringCommentRegexpRangesInLine(position.line);\n return isCharacterInIgnoredRanges(ranges, position.character);\n}\n\nfunction isCharacterInIgnoredRanges(\n ranges: [start: number, end: number][] | null,\n character: number\n): boolean {\n if (ranges === null) {\n return false;\n }\n for (const [start, end] of ranges) {\n if (character < start) {\n return false;\n }\n if (character < end) {\n return true;\n }\n }\n return false;\n}\n\nfunction isCharacterIgnoredForward(\n ranges: [start: number, end: number][] | null,\n character: number,\n cursor: { index: number }\n): boolean {\n if (ranges === null) {\n return false;\n }\n while (cursor.index < ranges.length && character >= ranges[cursor.index][1]) {\n cursor.index++;\n }\n const range = ranges[cursor.index];\n return range !== undefined && character >= range[0];\n}\n\nfunction isCharacterIgnoredBackward(\n ranges: [start: number, end: number][] | null,\n character: number,\n cursor: { index: number }\n): boolean {\n if (ranges === null) {\n return false;\n }\n while (cursor.index >= 0 && character < ranges[cursor.index][0]) {\n cursor.index--;\n }\n const range = ranges[cursor.index];\n return range !== undefined && character < range[1];\n}\n\nfunction createBracketMatchRanges(\n firstPosition: Position | undefined,\n secondPosition: Position | undefined\n): [open: Range, close: Range] | undefined {\n if (firstPosition === undefined || secondPosition === undefined) {\n return;\n }\n return [\n createCharacterRange(firstPosition),\n createCharacterRange(secondPosition),\n ];\n}\n\nfunction createCharacterRange(position: Position): Range {\n return {\n start: { line: position.line, character: position.character },\n end: { line: position.line, character: position.character + 1 },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/editor/popover.d.ts
|
|
2
|
+
declare const POPOVER_BOUNDARY_LINES = 3;
|
|
3
|
+
declare const POPOVER_FLIP_HYSTERESIS_PX = 4;
|
|
4
|
+
/**
|
|
5
|
+
* The visible scroll viewport expressed in the overlay's coordinate space (the
|
|
6
|
+
* same space --popover-y uses), or undefined when no usable layout geometry
|
|
7
|
+
* exists (e.g. a detached unit-test DOM, where every rect is zero).
|
|
8
|
+
*/
|
|
9
|
+
interface PopoverPlacementBounds {
|
|
10
|
+
/** The top of the popover in overlay coordinate space. */
|
|
11
|
+
top: number;
|
|
12
|
+
/** The bottom of the popover in overlay coordinate space. */
|
|
13
|
+
bottom: number;
|
|
14
|
+
}
|
|
15
|
+
interface PopoverManagerOptions {
|
|
16
|
+
hasActivePopover: () => boolean;
|
|
17
|
+
updateActivePopover: () => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Shared placement logic for the editor's overlay popovers: each anchors below
|
|
21
|
+
* or above a document position, flipping to the opposite side only when the
|
|
22
|
+
* preferred side would be clipped by the visible scrollport.
|
|
23
|
+
*/
|
|
24
|
+
declare class PopoverManager {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(options: PopoverManagerOptions);
|
|
27
|
+
setViewportElements(fileContainer: HTMLElement, codeElement: HTMLElement): void;
|
|
28
|
+
cleanUp(): void;
|
|
29
|
+
resetPlacement(): void;
|
|
30
|
+
setPlacement(placement: 'preferred' | 'fallback'): void;
|
|
31
|
+
choosePlacement(input: {
|
|
32
|
+
/** Bounds for the anchor's preferred side (e.g. above for backward, below for forward). */preferred: PopoverPlacementBounds; /** Bounds for the opposite edge we flip to when the preferred side has no room. */
|
|
33
|
+
fallback: PopoverPlacementBounds; /** The visible scrollport in overlay coordinate space, or undefined without layout geometry (e.g. a detached test DOM). */
|
|
34
|
+
viewport: PopoverPlacementBounds | undefined; /** The popover's measured height; 0 before it has laid out. */
|
|
35
|
+
popoverHeight: number; /** Whether the anchor is within the document's first/last rows; only used as a fallback signal when `viewport` is unavailable. */
|
|
36
|
+
atDocumentEdge: boolean;
|
|
37
|
+
}): 'preferred' | 'fallback';
|
|
38
|
+
/**
|
|
39
|
+
* Returns the bounds of the popover in overlay coordinate space.
|
|
40
|
+
*/
|
|
41
|
+
getPlacementBounds(): PopoverPlacementBounds | undefined;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { POPOVER_BOUNDARY_LINES, POPOVER_FLIP_HYSTERESIS_PX, PopoverManager, PopoverManagerOptions, PopoverPlacementBounds };
|
|
45
|
+
//# sourceMappingURL=popover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popover.d.ts","names":[],"sources":["../../src/editor/popover.ts"],"mappings":";cAIa,sBAAA;AAAA,cAKA,0BAAA;;;;AALsB;AAKnC;UAOiB,sBAAA;;EAEf,GAAA;EATqC;EAWrC,MAAM;AAAA;AAAA,UAGS,qBAAA;EACf,gBAAA;EACA,mBAAmB;AAAA;;;;AAAA;AAQrB;cAAa,cAAA;EAAA;EAcX,WAAA,CAAY,OAAA,EAAS,qBAAA;EAKrB,mBAAA,CACE,aAAA,EAAe,WAAA,EACf,WAAA,EAAa,WAAA;EAaf,OAAA;EAaA,cAAA;EAIA,YAAA,CAAa,SAAA;EAOb,eAAA,CAAgB,KAAA;IAwCM,2FAtCpB,SAAA,EAAW,sBAAA,EAsC+B;IApC1C,QAAA,EAAU,sBAAA,EAhDZ;IAkDE,QAAA,EAAU,sBAAA,cAlDA;IAoDV,aAAA,UA9Ce;IAgDf,cAAA;EAAA;EA/CA;;;EA6EF,kBAAA,IAAsB,sBAAA;AAAA"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { addEventListener } from "./utils.js";
|
|
2
|
+
//#region src/editor/popover.ts
|
|
3
|
+
const POPOVER_BOUNDARY_LINES = 3;
|
|
4
|
+
const POPOVER_FLIP_HYSTERESIS_PX = 4;
|
|
5
|
+
/**
|
|
6
|
+
* Shared placement logic for the editor's overlay popovers: each anchors below
|
|
7
|
+
* or above a document position, flipping to the opposite side only when the
|
|
8
|
+
* preferred side would be clipped by the visible scrollport.
|
|
9
|
+
*/
|
|
10
|
+
var PopoverManager = class {
|
|
11
|
+
#fileContainer;
|
|
12
|
+
#codeElement;
|
|
13
|
+
#scrollContainer;
|
|
14
|
+
#scrollContainerSource;
|
|
15
|
+
#cachedCodeRect;
|
|
16
|
+
#cachedScrollContainerRect;
|
|
17
|
+
#viewportRectsDirty = true;
|
|
18
|
+
#viewportRectListenersDisposes;
|
|
19
|
+
#placement;
|
|
20
|
+
#hasActivePopover;
|
|
21
|
+
#updateActivePopover;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.#hasActivePopover = options.hasActivePopover;
|
|
24
|
+
this.#updateActivePopover = options.updateActivePopover;
|
|
25
|
+
}
|
|
26
|
+
setViewportElements(fileContainer, codeElement) {
|
|
27
|
+
this.#fileContainer = fileContainer;
|
|
28
|
+
if (this.#codeElement === codeElement) return;
|
|
29
|
+
this.#codeElement = codeElement;
|
|
30
|
+
this.#viewportRectsDirty = true;
|
|
31
|
+
}
|
|
32
|
+
cleanUp() {
|
|
33
|
+
this.#codeElement = void 0;
|
|
34
|
+
this.#fileContainer = void 0;
|
|
35
|
+
this.#scrollContainer = void 0;
|
|
36
|
+
this.#scrollContainerSource = void 0;
|
|
37
|
+
this.#cachedCodeRect = void 0;
|
|
38
|
+
this.#cachedScrollContainerRect = void 0;
|
|
39
|
+
this.#viewportRectsDirty = true;
|
|
40
|
+
this.#viewportRectListenersDisposes?.forEach((dispose) => dispose());
|
|
41
|
+
this.#viewportRectListenersDisposes = void 0;
|
|
42
|
+
this.#placement = void 0;
|
|
43
|
+
}
|
|
44
|
+
resetPlacement() {
|
|
45
|
+
this.#placement = void 0;
|
|
46
|
+
}
|
|
47
|
+
setPlacement(placement) {
|
|
48
|
+
this.#placement = placement;
|
|
49
|
+
}
|
|
50
|
+
choosePlacement(input) {
|
|
51
|
+
const { preferred, fallback, viewport, popoverHeight, atDocumentEdge } = input;
|
|
52
|
+
let placement;
|
|
53
|
+
if (viewport !== void 0 && popoverHeight > 0) {
|
|
54
|
+
const fits = (bounds, margin = 0) => bounds.top >= viewport.top + margin && bounds.bottom <= viewport.bottom - margin;
|
|
55
|
+
if (this.#placement === "fallback" && fits(fallback) && !fits(preferred, 4)) placement = "fallback";
|
|
56
|
+
else if (!fits(preferred) && fits(fallback)) placement = "fallback";
|
|
57
|
+
else placement = "preferred";
|
|
58
|
+
} else placement = atDocumentEdge ? "fallback" : "preferred";
|
|
59
|
+
this.#placement = placement;
|
|
60
|
+
return placement;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns the bounds of the popover in overlay coordinate space.
|
|
64
|
+
*/
|
|
65
|
+
getPlacementBounds() {
|
|
66
|
+
const codeRect = this.#getCodeRect();
|
|
67
|
+
if (codeRect === void 0) return;
|
|
68
|
+
const scrollContainerRect = this.#getScrollContainerRect();
|
|
69
|
+
let topScreen;
|
|
70
|
+
let bottomScreen;
|
|
71
|
+
if (scrollContainerRect !== void 0) {
|
|
72
|
+
topScreen = scrollContainerRect.top;
|
|
73
|
+
bottomScreen = scrollContainerRect.bottom;
|
|
74
|
+
} else {
|
|
75
|
+
topScreen = 0;
|
|
76
|
+
bottomScreen = window.innerHeight;
|
|
77
|
+
}
|
|
78
|
+
if (bottomScreen <= topScreen) return;
|
|
79
|
+
return {
|
|
80
|
+
top: topScreen - codeRect.top,
|
|
81
|
+
bottom: bottomScreen - codeRect.top
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
#getScrollContainer() {
|
|
85
|
+
const fileContainer = this.#fileContainer;
|
|
86
|
+
if (fileContainer === void 0) return;
|
|
87
|
+
if (this.#scrollContainerSource === fileContainer) {
|
|
88
|
+
if (this.#scrollContainer === void 0 || this.#scrollContainer.isConnected) return this.#scrollContainer;
|
|
89
|
+
}
|
|
90
|
+
let element = fileContainer.parentElement;
|
|
91
|
+
while (element !== null) {
|
|
92
|
+
const overflowY = getComputedStyle(element).overflowY;
|
|
93
|
+
if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") {
|
|
94
|
+
this.#scrollContainer = element;
|
|
95
|
+
this.#scrollContainerSource = fileContainer;
|
|
96
|
+
return element;
|
|
97
|
+
}
|
|
98
|
+
element = element.parentElement;
|
|
99
|
+
}
|
|
100
|
+
this.#scrollContainer = void 0;
|
|
101
|
+
this.#scrollContainerSource = fileContainer;
|
|
102
|
+
}
|
|
103
|
+
#refreshViewportRectsIfNeeded() {
|
|
104
|
+
if (!this.#viewportRectsDirty) return;
|
|
105
|
+
this.#ensureViewportRectListeners();
|
|
106
|
+
this.#cachedCodeRect = this.#codeElement?.getBoundingClientRect();
|
|
107
|
+
this.#cachedScrollContainerRect = this.#getScrollContainer()?.getBoundingClientRect();
|
|
108
|
+
this.#viewportRectsDirty = false;
|
|
109
|
+
}
|
|
110
|
+
#getCodeRect() {
|
|
111
|
+
this.#refreshViewportRectsIfNeeded();
|
|
112
|
+
return this.#cachedCodeRect;
|
|
113
|
+
}
|
|
114
|
+
#getScrollContainerRect() {
|
|
115
|
+
this.#refreshViewportRectsIfNeeded();
|
|
116
|
+
return this.#cachedScrollContainerRect;
|
|
117
|
+
}
|
|
118
|
+
#ensureViewportRectListeners() {
|
|
119
|
+
if (this.#viewportRectListenersDisposes !== void 0) return;
|
|
120
|
+
let repositionRafId;
|
|
121
|
+
const markDirty = () => {
|
|
122
|
+
this.#viewportRectsDirty = true;
|
|
123
|
+
if (!this.#hasActivePopover() || repositionRafId !== void 0) return;
|
|
124
|
+
repositionRafId = requestAnimationFrame(() => {
|
|
125
|
+
repositionRafId = void 0;
|
|
126
|
+
this.#updateActivePopover();
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
const scroller = this.#getScrollContainer();
|
|
130
|
+
this.#viewportRectListenersDisposes = [
|
|
131
|
+
scroller !== void 0 ? addEventListener(scroller, "scroll", markDirty, { passive: true }) : addEventListener(window, "scroll", markDirty, { passive: true }),
|
|
132
|
+
addEventListener(window, "resize", markDirty, { passive: true }),
|
|
133
|
+
() => {
|
|
134
|
+
if (repositionRafId !== void 0) {
|
|
135
|
+
cancelAnimationFrame(repositionRafId);
|
|
136
|
+
repositionRafId = void 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
export { POPOVER_BOUNDARY_LINES, POPOVER_FLIP_HYSTERESIS_PX, PopoverManager };
|
|
144
|
+
|
|
145
|
+
//# sourceMappingURL=popover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popover.js","names":["#hasActivePopover","#updateActivePopover","#fileContainer","#codeElement","#viewportRectsDirty","#scrollContainer","#scrollContainerSource","#cachedCodeRect","#cachedScrollContainerRect","#viewportRectListenersDisposes","#placement","#getCodeRect","#getScrollContainerRect","#ensureViewportRectListeners","#getScrollContainer","#refreshViewportRectsIfNeeded"],"sources":["../../src/editor/popover.ts"],"sourcesContent":["import { addEventListener } from './utils';\n\n// Lines from the document edge at which the no-viewport fallback heuristic\n// (see `atDocumentEdge` below) flips placement.\nexport const POPOVER_BOUNDARY_LINES = 3;\n\n// Extra clearance (px) `preferred` must regain before a popover that flipped\n// to `fallback` flips back, so an anchor sitting right at the flip boundary\n// doesn't flicker between sides on every recompute.\nexport const POPOVER_FLIP_HYSTERESIS_PX = 4;\n\n/**\n * The visible scroll viewport expressed in the overlay's coordinate space (the\n * same space --popover-y uses), or undefined when no usable layout geometry\n * exists (e.g. a detached unit-test DOM, where every rect is zero).\n */\nexport interface PopoverPlacementBounds {\n /** The top of the popover in overlay coordinate space. */\n top: number;\n /** The bottom of the popover in overlay coordinate space. */\n bottom: number;\n}\n\nexport interface PopoverManagerOptions {\n hasActivePopover: () => boolean;\n updateActivePopover: () => void;\n}\n\n/**\n * Shared placement logic for the editor's overlay popovers: each anchors below\n * or above a document position, flipping to the opposite side only when the\n * preferred side would be clipped by the visible scrollport.\n */\nexport class PopoverManager {\n #fileContainer?: HTMLElement;\n #codeElement?: HTMLElement;\n #scrollContainer?: HTMLElement;\n #scrollContainerSource?: HTMLElement;\n #cachedCodeRect?: DOMRect;\n #cachedScrollContainerRect?: DOMRect;\n #viewportRectsDirty = true;\n #viewportRectListenersDisposes?: (() => void)[];\n #placement?: 'preferred' | 'fallback';\n\n readonly #hasActivePopover: () => boolean;\n readonly #updateActivePopover: () => void;\n\n constructor(options: PopoverManagerOptions) {\n this.#hasActivePopover = options.hasActivePopover;\n this.#updateActivePopover = options.updateActivePopover;\n }\n\n setViewportElements(\n fileContainer: HTMLElement,\n codeElement: HTMLElement\n ): void {\n this.#fileContainer = fileContainer;\n if (this.#codeElement === codeElement) {\n return;\n }\n this.#codeElement = codeElement;\n // A new [data-code] element invalidates the cached viewport rects (e.g. a\n // full re-render loaded a different file, possibly at a different on-screen\n // position) even without an intervening scroll/resize event.\n this.#viewportRectsDirty = true;\n }\n\n cleanUp(): void {\n this.#codeElement = undefined;\n this.#fileContainer = undefined;\n this.#scrollContainer = undefined;\n this.#scrollContainerSource = undefined;\n this.#cachedCodeRect = undefined;\n this.#cachedScrollContainerRect = undefined;\n this.#viewportRectsDirty = true;\n this.#viewportRectListenersDisposes?.forEach((dispose) => dispose());\n this.#viewportRectListenersDisposes = undefined;\n this.#placement = undefined;\n }\n\n resetPlacement(): void {\n this.#placement = undefined;\n }\n\n setPlacement(placement: 'preferred' | 'fallback'): void {\n this.#placement = placement;\n }\n\n // Flips to the opposite side only when the preferred side would be clipped by\n // the real scrollport and the fallback side fits; without viewport geometry,\n // falls back to the document-edge signal so the first/last rows still flip.\n choosePlacement(input: {\n /** Bounds for the anchor's preferred side (e.g. above for backward, below for forward). */\n preferred: PopoverPlacementBounds;\n /** Bounds for the opposite edge we flip to when the preferred side has no room. */\n fallback: PopoverPlacementBounds;\n /** The visible scrollport in overlay coordinate space, or undefined without layout geometry (e.g. a detached test DOM). */\n viewport: PopoverPlacementBounds | undefined;\n /** The popover's measured height; 0 before it has laid out. */\n popoverHeight: number;\n /** Whether the anchor is within the document's first/last rows; only used as a fallback signal when `viewport` is unavailable. */\n atDocumentEdge: boolean;\n }): 'preferred' | 'fallback' {\n const { preferred, fallback, viewport, popoverHeight, atDocumentEdge } =\n input;\n let placement: 'preferred' | 'fallback';\n if (viewport !== undefined && popoverHeight > 0) {\n const fits = (bounds: PopoverPlacementBounds, margin = 0): boolean =>\n bounds.top >= viewport.top + margin &&\n bounds.bottom <= viewport.bottom - margin;\n if (\n this.#placement === 'fallback' &&\n fits(fallback) &&\n !fits(preferred, POPOVER_FLIP_HYSTERESIS_PX)\n ) {\n placement = 'fallback';\n } else if (!fits(preferred) && fits(fallback)) {\n placement = 'fallback';\n } else {\n placement = 'preferred';\n }\n } else {\n placement = atDocumentEdge ? 'fallback' : 'preferred';\n }\n this.#placement = placement;\n return placement;\n }\n\n /**\n * Returns the bounds of the popover in overlay coordinate space.\n */\n getPlacementBounds(): PopoverPlacementBounds | undefined {\n const codeRect = this.#getCodeRect();\n if (codeRect === undefined) {\n return undefined;\n }\n const scrollContainerRect = this.#getScrollContainerRect();\n let topScreen: number;\n let bottomScreen: number;\n if (scrollContainerRect !== undefined) {\n topScreen = scrollContainerRect.top;\n bottomScreen = scrollContainerRect.bottom;\n } else {\n // No scrollable ancestor: the page itself scrolls, so the window is the\n // clip region.\n topScreen = 0;\n bottomScreen = window.innerHeight;\n }\n if (bottomScreen <= topScreen) {\n return undefined;\n }\n return {\n top: topScreen - codeRect.top,\n bottom: bottomScreen - codeRect.top,\n };\n }\n\n // The nearest scrollable ancestor of the file container -- the element whose\n // overflow clips overlay widgets. Walks the light-DOM ancestor chain (the\n // overlay lives in the file container's shadow root) and caches the result per\n // file container so it is not re-resolved on every render.\n //\n // Known limitations: this only inspects light-DOM ancestors outside the\n // shadow root, so it cannot see a clip boundary inside it (e.g. `[data-code]`\n // itself uses `overflow-y: clip`, which also is not one of the values matched\n // below) and cannot cross into an outer shadow root if the file container is\n // itself nested in another web component. It also only considers the\n // *nearest* scrollable ancestor, not the intersection of every clipping\n // ancestor, so a popover could still be clipped by an outer scroll context\n // even when it fits within this nearer one.\n #getScrollContainer(): HTMLElement | undefined {\n const fileContainer = this.#fileContainer;\n if (fileContainer === undefined) {\n return undefined;\n }\n if (this.#scrollContainerSource === fileContainer) {\n // A cached element can detach across re-mounts; re-resolve in that case\n // but otherwise trust the cache (including a cached \"no scroller\" result).\n if (\n this.#scrollContainer === undefined ||\n this.#scrollContainer.isConnected\n ) {\n return this.#scrollContainer;\n }\n }\n let element: HTMLElement | null = fileContainer.parentElement;\n while (element !== null) {\n const overflowY = getComputedStyle(element).overflowY;\n if (\n overflowY === 'auto' ||\n overflowY === 'scroll' ||\n overflowY === 'overlay'\n ) {\n this.#scrollContainer = element;\n this.#scrollContainerSource = fileContainer;\n return element;\n }\n element = element.parentElement;\n }\n this.#scrollContainer = undefined;\n this.#scrollContainerSource = fileContainer;\n return undefined;\n }\n\n // Lazily measures and caches the screen-space rects getOverlayViewport needs,\n // refreshing only when a scroll or resize event marks them dirty rather than\n // on every call. Selection-action placement can run once per keystroke while a\n // ranged selection stays open, so re-measuring via getBoundingClientRect on\n // every call would force a synchronous layout reflow on that hot path.\n #refreshViewportRectsIfNeeded(): void {\n if (!this.#viewportRectsDirty) {\n return;\n }\n this.#ensureViewportRectListeners();\n this.#cachedCodeRect = this.#codeElement?.getBoundingClientRect();\n this.#cachedScrollContainerRect =\n this.#getScrollContainer()?.getBoundingClientRect();\n this.#viewportRectsDirty = false;\n }\n\n #getCodeRect(): DOMRect | undefined {\n this.#refreshViewportRectsIfNeeded();\n return this.#cachedCodeRect;\n }\n\n #getScrollContainerRect(): DOMRect | undefined {\n this.#refreshViewportRectsIfNeeded();\n return this.#cachedScrollContainerRect;\n }\n\n // Subscribes once to events that can move or resize the cached rects: a scroll\n // on the resolved scroll container (or the window, when the page itself\n // scrolls) and a window resize. Scrolling/resizing can change whether the open\n // popover still fits, so this re-runs placement rAF-throttled.\n #ensureViewportRectListeners(): void {\n if (this.#viewportRectListenersDisposes !== undefined) {\n return;\n }\n let repositionRafId: number | undefined;\n const markDirty = () => {\n this.#viewportRectsDirty = true;\n if (!this.#hasActivePopover() || repositionRafId !== undefined) {\n return;\n }\n repositionRafId = requestAnimationFrame(() => {\n repositionRafId = undefined;\n this.#updateActivePopover();\n });\n };\n const scroller = this.#getScrollContainer();\n this.#viewportRectListenersDisposes = [\n scroller !== undefined\n ? addEventListener(scroller, 'scroll', markDirty, { passive: true })\n : addEventListener(window, 'scroll', markDirty, { passive: true }),\n addEventListener(window, 'resize', markDirty, { passive: true }),\n () => {\n if (repositionRafId !== undefined) {\n cancelAnimationFrame(repositionRafId);\n repositionRafId = undefined;\n }\n },\n ];\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"searchPanel.d.ts","names":[],"sources":["../../src/editor/searchPanel.ts"],"mappings":";;;KAOY,UAAA,IAAc,WAAA,UAAqB,SAAS;AAAA,KAE5C,eAAA;AAAA,UAEK,YAAA;EACf,IAAA;EACA,WAAA;EACA,aAAA;EACA,SAAA;EACA,KAAA;AAAA;AAAA,UAGe,kBAAA;EACf,YAAA,EAAc,YAAA;EACd,gBAAA,EAAkB,WAAA;EAClB,YAAA;EACA,IAAA,GAAO,eAAA;EACP,YAAA,GAAe,UAAA;EACf,aAAA,GAAgB,SAAA,EAAW,UAAA,EAAY,WAAA;EACvC,YAAA,GAAe,KAAA,EAAO,gBAAA;EACtB,QAAA,GACE,OAAA,EAAS,UAAA,IACT,OAAA;IAAY,aAAA;EAAA,MACT,UAAA;EACL,OAAA;AAAA;AAAA,cAGW,iBAAA;EAAA;
|
|
1
|
+
{"version":3,"file":"searchPanel.d.ts","names":[],"sources":["../../src/editor/searchPanel.ts"],"mappings":";;;KAOY,UAAA,IAAc,WAAA,UAAqB,SAAS;AAAA,KAE5C,eAAA;AAAA,UAEK,YAAA;EACf,IAAA;EACA,WAAA;EACA,aAAA;EACA,SAAA;EACA,KAAA;AAAA;AAAA,UAGe,kBAAA;EACf,YAAA,EAAc,YAAA;EACd,gBAAA,EAAkB,WAAA;EAClB,YAAA;EACA,IAAA,GAAO,eAAA;EACP,YAAA,GAAe,UAAA;EACf,aAAA,GAAgB,SAAA,EAAW,UAAA,EAAY,WAAA;EACvC,YAAA,GAAe,KAAA,EAAO,gBAAA;EACtB,QAAA,GACE,OAAA,EAAS,UAAA,IACT,OAAA;IAAY,aAAA;EAAA,MACT,UAAA;EACL,OAAA;AAAA;AAAA,cAGW,iBAAA;EAAA;EAQX,WAAA,CAAY,OAAA,EAAS,kBAAA;EAqYrB,KAAA;EAIA,QAAA,CAAS,YAAA;EAIT,aAAA,CAAc,OAAA;IAAY,aAAA;EAAA;EAI1B,OAAA,CAAQ,IAAA,EAAM,eAAe;EAO7B,KAAA;EAOA,OAAA;AAAA"}
|
|
@@ -10,6 +10,7 @@ var SearchPanelWidget = class {
|
|
|
10
10
|
#updateMatches;
|
|
11
11
|
#applyMode;
|
|
12
12
|
#navigate;
|
|
13
|
+
#close;
|
|
13
14
|
constructor(options) {
|
|
14
15
|
const { textDocument, containerElement, defaultQuery, mode = "find", initialMatch, scrollToMatch, applyReplace, onUpdate, onClose } = options;
|
|
15
16
|
const searchParams = {
|
|
@@ -35,12 +36,7 @@ var SearchPanelWidget = class {
|
|
|
35
36
|
const noMatches = matches.all.length === 0;
|
|
36
37
|
prevButton.disabled = noMatches;
|
|
37
38
|
nextButton.disabled = noMatches;
|
|
38
|
-
if (
|
|
39
|
-
matchResultElement.textContent = "No results";
|
|
40
|
-
matchResultElement.dataset.noMatches = "";
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
if (matches.all.length === 0) {
|
|
39
|
+
if (noMatches) {
|
|
44
40
|
matchResultElement.textContent = "No results";
|
|
45
41
|
matchResultElement.dataset.noMatches = "";
|
|
46
42
|
matches.current = void 0;
|
|
@@ -119,6 +115,7 @@ var SearchPanelWidget = class {
|
|
|
119
115
|
this.cleanup();
|
|
120
116
|
onClose();
|
|
121
117
|
};
|
|
118
|
+
this.#close = close;
|
|
122
119
|
const iconButton = (opts) => h("button", {
|
|
123
120
|
type: "button",
|
|
124
121
|
title: opts.label,
|
|
@@ -301,6 +298,9 @@ var SearchPanelWidget = class {
|
|
|
301
298
|
setMode(mode) {
|
|
302
299
|
this.#applyMode?.(mode);
|
|
303
300
|
}
|
|
301
|
+
close() {
|
|
302
|
+
this.#close?.();
|
|
303
|
+
}
|
|
304
304
|
cleanup() {
|
|
305
305
|
this.#container.remove();
|
|
306
306
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"searchPanel.js","names":["#updateMatches","#navigate","#inputElement","#applyMode","#container"],"sources":["../../src/editor/searchPanel.ts"],"sourcesContent":["import { resolveFindAgainShortcut } from './command';\nimport { buildSearchReplacementText } from './pieceTable';\nimport { isPrimaryModifier } from './platform';\nimport { getEditorIconSvg, type SVGSpriteNames } from './sprite';\nimport type { ResolvedTextEdit, TextDocument } from './textDocument';\nimport { h } from './utils';\n\nexport type MatchRange = [startOffset: number, endOffset: number];\n\nexport type SearchPanelMode = 'find' | 'replace';\n\nexport interface SearchParams {\n text: string;\n replaceText: string;\n caseSensitive: boolean;\n wholeWord: boolean;\n regex: boolean;\n}\n\nexport interface SearchPanelOptions {\n textDocument: TextDocument<unknown>;\n containerElement: HTMLElement;\n defaultQuery: string;\n mode?: SearchPanelMode;\n initialMatch?: MatchRange;\n scrollToMatch: (nextMatch: MatchRange, retainFocus: boolean) => void;\n applyReplace: (edits: ResolvedTextEdit[]) => void;\n onUpdate: (\n matches: MatchRange[],\n options?: { syncSelection?: boolean }\n ) => MatchRange | undefined;\n onClose: () => void;\n}\n\nexport class SearchPanelWidget {\n #container: HTMLDivElement;\n #inputElement: HTMLInputElement;\n #updateMatches?: (options?: { syncSelection?: boolean }) => void;\n #applyMode?: (mode: SearchPanelMode) => void;\n #navigate?: (findPrevious: boolean) => void;\n\n constructor(options: SearchPanelOptions) {\n const {\n textDocument,\n containerElement,\n defaultQuery,\n mode = 'find',\n initialMatch,\n scrollToMatch,\n applyReplace,\n onUpdate,\n onClose,\n } = options;\n\n const searchParams: SearchParams = {\n text: defaultQuery,\n replaceText: '',\n caseSensitive: false,\n wholeWord: false,\n regex: false,\n };\n\n const matches = {\n all: [] as MatchRange[],\n current: undefined as MatchRange | undefined,\n };\n\n // Default to the empty-query \"no results\" state so it shows on open before\n // any search runs.\n const matchResultElement = h('div', {\n dataset: { matches: '', noMatches: '' },\n textContent: 'No results',\n });\n const updateMatches = (options?: { syncSelection?: boolean }) => {\n matches.all =\n searchParams.text !== '' ? textDocument.search(searchParams) : [];\n const noMatches = matches.all.length === 0;\n prevButton.disabled = noMatches;\n nextButton.disabled = noMatches;\n\n if (searchParams.text === '') {\n matchResultElement.textContent = 'No results';\n matchResultElement.dataset.noMatches = '';\n return;\n }\n\n if (matches.all.length === 0) {\n matchResultElement.textContent = 'No results';\n matchResultElement.dataset.noMatches = '';\n matches.current = undefined;\n onUpdate([]);\n return;\n }\n\n delete matchResultElement.dataset.noMatches;\n if (options?.syncSelection === false) {\n const currentMatch = onUpdate(matches.all, { syncSelection: false });\n updateCurrentMatch(currentMatch);\n return;\n }\n\n updateCurrentMatch(onUpdate(matches.all));\n };\n this.#updateMatches = updateMatches;\n\n const updateCurrentMatch = (currentMatch: MatchRange | undefined) => {\n if (currentMatch === undefined) {\n matchResultElement.textContent = `${matches.all.length} results`;\n } else {\n const [start, end] = currentMatch;\n const index = matches.all.findIndex(\n (m) => m[0] === start && m[1] === end\n );\n matchResultElement.textContent = `${index + 1} of ${matches.all.length}`;\n }\n matches.current = currentMatch;\n };\n\n const updateSearchParam = <K extends keyof SearchParams>(\n key: K,\n value: SearchParams[K]\n ) => {\n searchParams[key] = value;\n updateMatches();\n };\n\n const findNextMatch = (\n findPrevious: boolean = false,\n retainFocus: boolean = false\n ) => {\n const allMatches = matches.all;\n let nextMatch: MatchRange | undefined = allMatches[0];\n if (allMatches.length > 0) {\n if (findPrevious) {\n const searchOffset = matches.current?.[0] ?? 0;\n nextMatch = allMatches.at(-1);\n for (const m of allMatches) {\n if (m[1] <= searchOffset) {\n nextMatch = m;\n } else {\n break;\n }\n }\n } else {\n const searchOffset = matches.current?.[1] ?? 0;\n for (const m of allMatches) {\n if (m[0] >= searchOffset) {\n nextMatch = m;\n break;\n }\n }\n }\n }\n if (nextMatch !== undefined) {\n updateCurrentMatch(nextMatch);\n scrollToMatch(nextMatch, retainFocus);\n }\n matches.current = nextMatch;\n };\n // Keep focus in the panel when stepping through matches from the inputs.\n this.#navigate = (findPrevious: boolean) =>\n findNextMatch(findPrevious, true);\n\n const buildReplacementEdit = (\n matchStart: number,\n matchEnd: number\n ): ResolvedTextEdit => ({\n start: matchStart,\n end: matchEnd,\n text: buildSearchReplacementText(\n (offset) => textDocument.positionAt(offset),\n (position) => textDocument.offsetAt(position),\n (line) => textDocument.getLineText(line),\n searchParams,\n matchStart,\n matchEnd\n ),\n });\n\n const replace = () => {\n if (searchParams.text === '' || matches.all.length === 0) {\n return;\n }\n\n let currentMatch = matches.current;\n if (currentMatch === undefined) {\n findNextMatch(false, true);\n currentMatch = matches.current;\n if (currentMatch === undefined) {\n return;\n }\n }\n\n const [start, end] = currentMatch;\n const edit = buildReplacementEdit(start, end);\n applyReplace([edit]);\n\n // Collapse after the replacement so the next search pass advances.\n scrollToMatch([start + edit.text.length, start + edit.text.length], true);\n matches.current = undefined;\n updateMatches();\n };\n\n const replaceAll = () => {\n if (searchParams.text === '' || matches.all.length === 0) {\n return;\n }\n\n applyReplace(\n matches.all.map(([start, end]) => buildReplacementEdit(start, end))\n );\n matches.current = undefined;\n updateMatches();\n };\n\n const close = () => {\n this.cleanup();\n onClose();\n };\n\n // Shared builder for the panel's icon controls. Every clickable control is a\n // real <button> so it gets focus, keyboard activation, and native disabled\n // handling for free; `getEditorIconSvg` marks the glyph aria-hidden, so the\n // accessible name comes from `label` (also used as the hover title).\n const iconButton = (opts: {\n icon: SVGSpriteNames;\n label: string;\n size?: number;\n dataset?: Record<string, string>;\n onClick: () => void;\n }) =>\n h('button', {\n type: 'button',\n title: opts.label,\n ariaLabel: opts.label,\n dataset: { searchIcon: '', ...opts.dataset },\n innerHTML: getEditorIconSvg(opts.icon, opts.size ?? 16),\n onclick: opts.onClick,\n });\n\n // Builds an always-visible icon button that toggles one boolean search\n // option (case/whole-word/regex). The button reflects its on/off state via\n // `aria-pressed` so the stylesheet can highlight it and assistive tech can\n // announce it.\n const makeToggle = (\n icon: SVGSpriteNames,\n title: string,\n key: 'caseSensitive' | 'wholeWord' | 'regex'\n ) => {\n const button = iconButton({\n icon,\n label: title,\n size: 14,\n onClick: () => {\n const next = !searchParams[key];\n button.ariaPressed = String(next);\n updateSearchParam(key, next);\n },\n });\n button.ariaPressed = String(searchParams[key]);\n return button;\n };\n\n const caseSensitiveToggle = makeToggle(\n 'case',\n 'Match Case',\n 'caseSensitive'\n );\n const wholeWordToggle = makeToggle('whole-word', 'Whole Word', 'wholeWord');\n const regexToggle = makeToggle('regex', 'Regexp', 'regex');\n\n const replaceInputElement = h('input', {\n type: 'text',\n placeholder: 'Replace',\n dataset: 'replace',\n value: '',\n oninput: (e: Event) => {\n searchParams.replaceText = (e.target as HTMLInputElement).value;\n },\n onkeydown: (e: KeyboardEvent) => {\n const findAgain = resolveFindAgainShortcut(e);\n if (findAgain !== undefined) {\n e.preventDefault();\n findNextMatch(findAgain === 'previous', true);\n }\n },\n });\n\n this.#inputElement = h('input', {\n type: 'text',\n placeholder: 'Search',\n dataset: 'search',\n value: defaultQuery,\n oninput: (e: Event) => {\n searchParams.text = (e.target as HTMLInputElement).value;\n matches.current = undefined;\n updateMatches();\n },\n onkeydown: (e: KeyboardEvent) => {\n const findAgain = resolveFindAgainShortcut(e);\n if (e.key === 'Escape') {\n e.preventDefault();\n close();\n } else if (e.key === 'Enter') {\n e.preventDefault();\n findNextMatch(false, true);\n } else if (findAgain !== undefined) {\n // Override the native browser find-again shortcut so cmd+g steps to\n // the next match and cmd+shift+g to the previous one.\n e.preventDefault();\n findNextMatch(findAgain === 'previous', true);\n } else if (\n isPrimaryModifier(e) &&\n (e.key === 'f' || e.code === 'KeyF')\n ) {\n // Prevent the default browser search panel and switch the panel mode\n // in place (cmd+f -> find, cmd+opt+f -> find/replace).\n e.preventDefault();\n applyMode(e.altKey ? 'replace' : 'find');\n }\n },\n });\n\n // The three search-option toggles are overlaid on the right edge of the\n // find input instead of sitting beside it.\n const searchTogglesElement = h('div', {\n dataset: 'searchToggles',\n children: [caseSensitiveToggle, wholeWordToggle, regexToggle],\n });\n\n const findInputBox = h('div', {\n dataset: { inputBox: '', find: '' },\n children: [this.#inputElement, searchTogglesElement],\n });\n\n // The replace input and its action buttons are tagged as replace cells so\n // they can be hidden together when the panel is in find-only mode.\n const replaceInputBox = h('div', {\n dataset: { inputBox: '', replace: '', replaceCell: '' },\n children: [replaceInputElement],\n });\n\n const replaceActionsElement = h('div', {\n dataset: { replaceActions: '', replaceCell: '' },\n children: [\n iconButton({ icon: 'replace', label: 'Replace', onClick: replace }),\n iconButton({\n icon: 'replace-all',\n label: 'Replace All',\n onClick: replaceAll,\n }),\n ],\n });\n\n // Held so the no-results state can toggle their native disabled flag.\n const prevButton = iconButton({\n icon: 'arrow-up',\n label: 'Previous',\n onClick: () => {\n findNextMatch(true);\n },\n });\n const nextButton = iconButton({\n icon: 'arrow-down',\n label: 'Next',\n onClick: () => {\n findNextMatch();\n },\n });\n prevButton.disabled = true;\n nextButton.disabled = true;\n\n const navElement = h('div', {\n dataset: 'searchNav',\n children: [prevButton, nextButton],\n });\n\n const closeElement = iconButton({\n icon: 'close',\n label: 'Close',\n onClick: close,\n dataset: { searchClose: '' },\n });\n\n // Cells are positioned by CSS grid-template-areas (see editor.css), so DOM\n // order here only drives tab/reading order, not layout. Keep the replace\n // input directly after the find input (and its toggles) so Tab walks\n // find -> replace before reaching the nav arrows and close button.\n const gridElement = h('div', {\n dataset: { searchGrid: '', mode },\n children: [\n findInputBox,\n replaceInputBox,\n replaceActionsElement,\n matchResultElement,\n navElement,\n closeElement,\n ],\n });\n\n // Toggles the panel between find and find/replace modes by showing or\n // hiding the replace cells, then returns focus to the find input.\n const applyMode = (next: SearchPanelMode) => {\n gridElement.dataset.mode = next;\n this.#inputElement.focus();\n this.#inputElement.select();\n };\n this.#applyMode = applyMode;\n\n this.#container = h('div', {\n dataset: 'searchPanel',\n children: [\n h('div', {\n dataset: 'editorWidget',\n children: [gridElement],\n }),\n ],\n });\n\n matches.current = initialMatch;\n containerElement.before(this.#container);\n\n requestAnimationFrame(() => {\n if (initialMatch !== undefined) {\n updateMatches();\n } else {\n onUpdate([]);\n }\n this.#inputElement.select();\n });\n }\n\n focus(): void {\n this.#inputElement.focus();\n }\n\n navigate(findPrevious: boolean): void {\n this.#navigate?.(findPrevious);\n }\n\n updateMatches(options?: { syncSelection?: boolean }): void {\n this.#updateMatches?.(options);\n }\n\n setMode(mode: SearchPanelMode): void {\n this.#applyMode?.(mode);\n }\n\n cleanup(): void {\n this.#container.remove();\n }\n}\n"],"mappings":";;;;;;AAkCA,IAAa,oBAAb,MAA+B;CAC7B;CACA;CACA;CACA;CACA;CAEA,YAAY,SAA6B;EACvC,MAAM,EACJ,cACA,kBACA,cACA,OAAO,QACP,cACA,eACA,cACA,UACA,YACE;EAEJ,MAAM,eAA6B;GACjC,MAAM;GACN,aAAa;GACb,eAAe;GACf,WAAW;GACX,OAAO;EACT;EAEA,MAAM,UAAU;GACd,KAAK,CAAC;GACN,SAAS,KAAA;EACX;EAIA,MAAM,qBAAqB,EAAE,OAAO;GAClC,SAAS;IAAE,SAAS;IAAI,WAAW;GAAG;GACtC,aAAa;EACf,CAAC;EACD,MAAM,iBAAiB,YAA0C;GAC/D,QAAQ,MACN,aAAa,SAAS,KAAK,aAAa,OAAO,YAAY,IAAI,CAAC;GAClE,MAAM,YAAY,QAAQ,IAAI,WAAW;GACzC,WAAW,WAAW;GACtB,WAAW,WAAW;GAEtB,IAAI,aAAa,SAAS,IAAI;IAC5B,mBAAmB,cAAc;IACjC,mBAAmB,QAAQ,YAAY;IACvC;GACF;GAEA,IAAI,QAAQ,IAAI,WAAW,GAAG;IAC5B,mBAAmB,cAAc;IACjC,mBAAmB,QAAQ,YAAY;IACvC,QAAQ,UAAU,KAAA;IAClB,SAAS,CAAC,CAAC;IACX;GACF;GAEA,OAAO,mBAAmB,QAAQ;GAClC,IAAI,SAAS,kBAAkB,OAAO;IAEpC,mBADqB,SAAS,QAAQ,KAAK,EAAE,eAAe,MAAM,CACpC,CAAC;IAC/B;GACF;GAEA,mBAAmB,SAAS,QAAQ,GAAG,CAAC;EAC1C;EACA,KAAKA,iBAAiB;EAEtB,MAAM,sBAAsB,iBAAyC;GACnE,IAAI,iBAAiB,KAAA,GACnB,mBAAmB,cAAc,GAAG,QAAQ,IAAI,OAAO;QAClD;IACL,MAAM,CAAC,OAAO,OAAO;IAIrB,mBAAmB,cAAc,GAHnB,QAAQ,IAAI,WACvB,MAAM,EAAE,OAAO,SAAS,EAAE,OAAO,GAEI,IAAI,EAAE,MAAM,QAAQ,IAAI;GAClE;GACA,QAAQ,UAAU;EACpB;EAEA,MAAM,qBACJ,KACA,UACG;GACH,aAAa,OAAO;GACpB,cAAc;EAChB;EAEA,MAAM,iBACJ,eAAwB,OACxB,cAAuB,UACpB;GACH,MAAM,aAAa,QAAQ;GAC3B,IAAI,YAAoC,WAAW;GACnD,IAAI,WAAW,SAAS,GACtB,IAAI,cAAc;IAChB,MAAM,eAAe,QAAQ,UAAU,MAAM;IAC7C,YAAY,WAAW,GAAG,EAAE;IAC5B,KAAK,MAAM,KAAK,YACd,IAAI,EAAE,MAAM,cACV,YAAY;SAEZ;GAGN,OAAO;IACL,MAAM,eAAe,QAAQ,UAAU,MAAM;IAC7C,KAAK,MAAM,KAAK,YACd,IAAI,EAAE,MAAM,cAAc;KACxB,YAAY;KACZ;IACF;GAEJ;GAEF,IAAI,cAAc,KAAA,GAAW;IAC3B,mBAAmB,SAAS;IAC5B,cAAc,WAAW,WAAW;GACtC;GACA,QAAQ,UAAU;EACpB;EAEA,KAAKC,aAAa,iBAChB,cAAc,cAAc,IAAI;EAElC,MAAM,wBACJ,YACA,cACsB;GACtB,OAAO;GACP,KAAK;GACL,MAAM,4BACH,WAAW,aAAa,WAAW,MAAM,IACzC,aAAa,aAAa,SAAS,QAAQ,IAC3C,SAAS,aAAa,YAAY,IAAI,GACvC,cACA,YACA,QACF;EACF;EAEA,MAAM,gBAAgB;GACpB,IAAI,aAAa,SAAS,MAAM,QAAQ,IAAI,WAAW,GACrD;GAGF,IAAI,eAAe,QAAQ;GAC3B,IAAI,iBAAiB,KAAA,GAAW;IAC9B,cAAc,OAAO,IAAI;IACzB,eAAe,QAAQ;IACvB,IAAI,iBAAiB,KAAA,GACnB;GAEJ;GAEA,MAAM,CAAC,OAAO,OAAO;GACrB,MAAM,OAAO,qBAAqB,OAAO,GAAG;GAC5C,aAAa,CAAC,IAAI,CAAC;GAGnB,cAAc,CAAC,QAAQ,KAAK,KAAK,QAAQ,QAAQ,KAAK,KAAK,MAAM,GAAG,IAAI;GACxE,QAAQ,UAAU,KAAA;GAClB,cAAc;EAChB;EAEA,MAAM,mBAAmB;GACvB,IAAI,aAAa,SAAS,MAAM,QAAQ,IAAI,WAAW,GACrD;GAGF,aACE,QAAQ,IAAI,KAAK,CAAC,OAAO,SAAS,qBAAqB,OAAO,GAAG,CAAC,CACpE;GACA,QAAQ,UAAU,KAAA;GAClB,cAAc;EAChB;EAEA,MAAM,cAAc;GAClB,KAAK,QAAQ;GACb,QAAQ;EACV;EAMA,MAAM,cAAc,SAOlB,EAAE,UAAU;GACV,MAAM;GACN,OAAO,KAAK;GACZ,WAAW,KAAK;GAChB,SAAS;IAAE,YAAY;IAAI,GAAG,KAAK;GAAQ;GAC3C,WAAW,iBAAiB,KAAK,MAAM,KAAK,QAAQ,EAAE;GACtD,SAAS,KAAK;EAChB,CAAC;EAMH,MAAM,cACJ,MACA,OACA,QACG;GACH,MAAM,SAAS,WAAW;IACxB;IACA,OAAO;IACP,MAAM;IACN,eAAe;KACb,MAAM,OAAO,CAAC,aAAa;KAC3B,OAAO,cAAc,OAAO,IAAI;KAChC,kBAAkB,KAAK,IAAI;IAC7B;GACF,CAAC;GACD,OAAO,cAAc,OAAO,aAAa,IAAI;GAC7C,OAAO;EACT;EAEA,MAAM,sBAAsB,WAC1B,QACA,cACA,eACF;EACA,MAAM,kBAAkB,WAAW,cAAc,cAAc,WAAW;EAC1E,MAAM,cAAc,WAAW,SAAS,UAAU,OAAO;EAEzD,MAAM,sBAAsB,EAAE,SAAS;GACrC,MAAM;GACN,aAAa;GACb,SAAS;GACT,OAAO;GACP,UAAU,MAAa;IACrB,aAAa,cAAe,EAAE,OAA4B;GAC5D;GACA,YAAY,MAAqB;IAC/B,MAAM,YAAY,yBAAyB,CAAC;IAC5C,IAAI,cAAc,KAAA,GAAW;KAC3B,EAAE,eAAe;KACjB,cAAc,cAAc,YAAY,IAAI;IAC9C;GACF;EACF,CAAC;EAED,KAAKC,gBAAgB,EAAE,SAAS;GAC9B,MAAM;GACN,aAAa;GACb,SAAS;GACT,OAAO;GACP,UAAU,MAAa;IACrB,aAAa,OAAQ,EAAE,OAA4B;IACnD,QAAQ,UAAU,KAAA;IAClB,cAAc;GAChB;GACA,YAAY,MAAqB;IAC/B,MAAM,YAAY,yBAAyB,CAAC;IAC5C,IAAI,EAAE,QAAQ,UAAU;KACtB,EAAE,eAAe;KACjB,MAAM;IACR,OAAO,IAAI,EAAE,QAAQ,SAAS;KAC5B,EAAE,eAAe;KACjB,cAAc,OAAO,IAAI;IAC3B,OAAO,IAAI,cAAc,KAAA,GAAW;KAGlC,EAAE,eAAe;KACjB,cAAc,cAAc,YAAY,IAAI;IAC9C,OAAO,IACL,kBAAkB,CAAC,MAClB,EAAE,QAAQ,OAAO,EAAE,SAAS,SAC7B;KAGA,EAAE,eAAe;KACjB,UAAU,EAAE,SAAS,YAAY,MAAM;IACzC;GACF;EACF,CAAC;EAID,MAAM,uBAAuB,EAAE,OAAO;GACpC,SAAS;GACT,UAAU;IAAC;IAAqB;IAAiB;GAAW;EAC9D,CAAC;EAED,MAAM,eAAe,EAAE,OAAO;GAC5B,SAAS;IAAE,UAAU;IAAI,MAAM;GAAG;GAClC,UAAU,CAAC,KAAKA,eAAe,oBAAoB;EACrD,CAAC;EAID,MAAM,kBAAkB,EAAE,OAAO;GAC/B,SAAS;IAAE,UAAU;IAAI,SAAS;IAAI,aAAa;GAAG;GACtD,UAAU,CAAC,mBAAmB;EAChC,CAAC;EAED,MAAM,wBAAwB,EAAE,OAAO;GACrC,SAAS;IAAE,gBAAgB;IAAI,aAAa;GAAG;GAC/C,UAAU,CACR,WAAW;IAAE,MAAM;IAAW,OAAO;IAAW,SAAS;GAAQ,CAAC,GAClE,WAAW;IACT,MAAM;IACN,OAAO;IACP,SAAS;GACX,CAAC,CACH;EACF,CAAC;EAGD,MAAM,aAAa,WAAW;GAC5B,MAAM;GACN,OAAO;GACP,eAAe;IACb,cAAc,IAAI;GACpB;EACF,CAAC;EACD,MAAM,aAAa,WAAW;GAC5B,MAAM;GACN,OAAO;GACP,eAAe;IACb,cAAc;GAChB;EACF,CAAC;EACD,WAAW,WAAW;EACtB,WAAW,WAAW;EAEtB,MAAM,aAAa,EAAE,OAAO;GAC1B,SAAS;GACT,UAAU,CAAC,YAAY,UAAU;EACnC,CAAC;EAED,MAAM,eAAe,WAAW;GAC9B,MAAM;GACN,OAAO;GACP,SAAS;GACT,SAAS,EAAE,aAAa,GAAG;EAC7B,CAAC;EAMD,MAAM,cAAc,EAAE,OAAO;GAC3B,SAAS;IAAE,YAAY;IAAI;GAAK;GAChC,UAAU;IACR;IACA;IACA;IACA;IACA;IACA;GACF;EACF,CAAC;EAID,MAAM,aAAa,SAA0B;GAC3C,YAAY,QAAQ,OAAO;GAC3B,KAAKA,cAAc,MAAM;GACzB,KAAKA,cAAc,OAAO;EAC5B;EACA,KAAKC,aAAa;EAElB,KAAKC,aAAa,EAAE,OAAO;GACzB,SAAS;GACT,UAAU,CACR,EAAE,OAAO;IACP,SAAS;IACT,UAAU,CAAC,WAAW;GACxB,CAAC,CACH;EACF,CAAC;EAED,QAAQ,UAAU;EAClB,iBAAiB,OAAO,KAAKA,UAAU;EAEvC,4BAA4B;GAC1B,IAAI,iBAAiB,KAAA,GACnB,cAAc;QAEd,SAAS,CAAC,CAAC;GAEb,KAAKF,cAAc,OAAO;EAC5B,CAAC;CACH;CAEA,QAAc;EACZ,KAAKA,cAAc,MAAM;CAC3B;CAEA,SAAS,cAA6B;EACpC,KAAKD,YAAY,YAAY;CAC/B;CAEA,cAAc,SAA6C;EACzD,KAAKD,iBAAiB,OAAO;CAC/B;CAEA,QAAQ,MAA6B;EACnC,KAAKG,aAAa,IAAI;CACxB;CAEA,UAAgB;EACd,KAAKC,WAAW,OAAO;CACzB;AACF"}
|
|
1
|
+
{"version":3,"file":"searchPanel.js","names":["#updateMatches","#navigate","#close","#inputElement","#applyMode","#container"],"sources":["../../src/editor/searchPanel.ts"],"sourcesContent":["import { resolveFindAgainShortcut } from './command';\nimport { buildSearchReplacementText } from './pieceTable';\nimport { isPrimaryModifier } from './platform';\nimport { getEditorIconSvg, type SVGSpriteNames } from './sprite';\nimport type { ResolvedTextEdit, TextDocument } from './textDocument';\nimport { h } from './utils';\n\nexport type MatchRange = [startOffset: number, endOffset: number];\n\nexport type SearchPanelMode = 'find' | 'replace';\n\nexport interface SearchParams {\n text: string;\n replaceText: string;\n caseSensitive: boolean;\n wholeWord: boolean;\n regex: boolean;\n}\n\nexport interface SearchPanelOptions {\n textDocument: TextDocument<unknown>;\n containerElement: HTMLElement;\n defaultQuery: string;\n mode?: SearchPanelMode;\n initialMatch?: MatchRange;\n scrollToMatch: (nextMatch: MatchRange, retainFocus: boolean) => void;\n applyReplace: (edits: ResolvedTextEdit[]) => void;\n onUpdate: (\n matches: MatchRange[],\n options?: { syncSelection?: boolean }\n ) => MatchRange | undefined;\n onClose: () => void;\n}\n\nexport class SearchPanelWidget {\n #container: HTMLDivElement;\n #inputElement: HTMLInputElement;\n #updateMatches?: (options?: { syncSelection?: boolean }) => void;\n #applyMode?: (mode: SearchPanelMode) => void;\n #navigate?: (findPrevious: boolean) => void;\n #close?: () => void;\n\n constructor(options: SearchPanelOptions) {\n const {\n textDocument,\n containerElement,\n defaultQuery,\n mode = 'find',\n initialMatch,\n scrollToMatch,\n applyReplace,\n onUpdate,\n onClose,\n } = options;\n\n const searchParams: SearchParams = {\n text: defaultQuery,\n replaceText: '',\n caseSensitive: false,\n wholeWord: false,\n regex: false,\n };\n\n const matches = {\n all: [] as MatchRange[],\n current: undefined as MatchRange | undefined,\n };\n\n // Default to the empty-query \"no results\" state so it shows on open before\n // any search runs.\n const matchResultElement = h('div', {\n dataset: { matches: '', noMatches: '' },\n textContent: 'No results',\n });\n const updateMatches = (options?: { syncSelection?: boolean }) => {\n matches.all =\n searchParams.text !== '' ? textDocument.search(searchParams) : [];\n const noMatches = matches.all.length === 0;\n prevButton.disabled = noMatches;\n nextButton.disabled = noMatches;\n\n // An empty query and a query with zero hits are the same \"no results\"\n // state. Both push an empty match set through onUpdate so the editor\n // drops #matches and clears the painted highlights.\n if (noMatches) {\n matchResultElement.textContent = 'No results';\n matchResultElement.dataset.noMatches = '';\n matches.current = undefined;\n onUpdate([]);\n return;\n }\n\n delete matchResultElement.dataset.noMatches;\n if (options?.syncSelection === false) {\n const currentMatch = onUpdate(matches.all, { syncSelection: false });\n updateCurrentMatch(currentMatch);\n return;\n }\n\n updateCurrentMatch(onUpdate(matches.all));\n };\n this.#updateMatches = updateMatches;\n\n const updateCurrentMatch = (currentMatch: MatchRange | undefined) => {\n if (currentMatch === undefined) {\n matchResultElement.textContent = `${matches.all.length} results`;\n } else {\n const [start, end] = currentMatch;\n const index = matches.all.findIndex(\n (m) => m[0] === start && m[1] === end\n );\n matchResultElement.textContent = `${index + 1} of ${matches.all.length}`;\n }\n matches.current = currentMatch;\n };\n\n const updateSearchParam = <K extends keyof SearchParams>(\n key: K,\n value: SearchParams[K]\n ) => {\n searchParams[key] = value;\n updateMatches();\n };\n\n const findNextMatch = (\n findPrevious: boolean = false,\n retainFocus: boolean = false\n ) => {\n const allMatches = matches.all;\n let nextMatch: MatchRange | undefined = allMatches[0];\n if (allMatches.length > 0) {\n if (findPrevious) {\n const searchOffset = matches.current?.[0] ?? 0;\n nextMatch = allMatches.at(-1);\n for (const m of allMatches) {\n if (m[1] <= searchOffset) {\n nextMatch = m;\n } else {\n break;\n }\n }\n } else {\n const searchOffset = matches.current?.[1] ?? 0;\n for (const m of allMatches) {\n if (m[0] >= searchOffset) {\n nextMatch = m;\n break;\n }\n }\n }\n }\n if (nextMatch !== undefined) {\n updateCurrentMatch(nextMatch);\n scrollToMatch(nextMatch, retainFocus);\n }\n matches.current = nextMatch;\n };\n // Keep focus in the panel when stepping through matches from the inputs.\n this.#navigate = (findPrevious: boolean) =>\n findNextMatch(findPrevious, true);\n\n const buildReplacementEdit = (\n matchStart: number,\n matchEnd: number\n ): ResolvedTextEdit => ({\n start: matchStart,\n end: matchEnd,\n text: buildSearchReplacementText(\n (offset) => textDocument.positionAt(offset),\n (position) => textDocument.offsetAt(position),\n (line) => textDocument.getLineText(line),\n searchParams,\n matchStart,\n matchEnd\n ),\n });\n\n const replace = () => {\n if (searchParams.text === '' || matches.all.length === 0) {\n return;\n }\n\n let currentMatch = matches.current;\n if (currentMatch === undefined) {\n findNextMatch(false, true);\n currentMatch = matches.current;\n if (currentMatch === undefined) {\n return;\n }\n }\n\n const [start, end] = currentMatch;\n const edit = buildReplacementEdit(start, end);\n applyReplace([edit]);\n\n // Collapse after the replacement so the next search pass advances.\n scrollToMatch([start + edit.text.length, start + edit.text.length], true);\n matches.current = undefined;\n updateMatches();\n };\n\n const replaceAll = () => {\n if (searchParams.text === '' || matches.all.length === 0) {\n return;\n }\n\n applyReplace(\n matches.all.map(([start, end]) => buildReplacementEdit(start, end))\n );\n matches.current = undefined;\n updateMatches();\n };\n\n const close = () => {\n this.cleanup();\n onClose();\n };\n this.#close = close;\n\n // Shared builder for the panel's icon controls. Every clickable control is a\n // real <button> so it gets focus, keyboard activation, and native disabled\n // handling for free; `getEditorIconSvg` marks the glyph aria-hidden, so the\n // accessible name comes from `label` (also used as the hover title).\n const iconButton = (opts: {\n icon: SVGSpriteNames;\n label: string;\n size?: number;\n dataset?: Record<string, string>;\n onClick: () => void;\n }) =>\n h('button', {\n type: 'button',\n title: opts.label,\n ariaLabel: opts.label,\n dataset: { searchIcon: '', ...opts.dataset },\n innerHTML: getEditorIconSvg(opts.icon, opts.size ?? 16),\n onclick: opts.onClick,\n });\n\n // Builds an always-visible icon button that toggles one boolean search\n // option (case/whole-word/regex). The button reflects its on/off state via\n // `aria-pressed` so the stylesheet can highlight it and assistive tech can\n // announce it.\n const makeToggle = (\n icon: SVGSpriteNames,\n title: string,\n key: 'caseSensitive' | 'wholeWord' | 'regex'\n ) => {\n const button = iconButton({\n icon,\n label: title,\n size: 14,\n onClick: () => {\n const next = !searchParams[key];\n button.ariaPressed = String(next);\n updateSearchParam(key, next);\n },\n });\n button.ariaPressed = String(searchParams[key]);\n return button;\n };\n\n const caseSensitiveToggle = makeToggle(\n 'case',\n 'Match Case',\n 'caseSensitive'\n );\n const wholeWordToggle = makeToggle('whole-word', 'Whole Word', 'wholeWord');\n const regexToggle = makeToggle('regex', 'Regexp', 'regex');\n\n const replaceInputElement = h('input', {\n type: 'text',\n placeholder: 'Replace',\n dataset: 'replace',\n value: '',\n oninput: (e: Event) => {\n searchParams.replaceText = (e.target as HTMLInputElement).value;\n },\n onkeydown: (e: KeyboardEvent) => {\n const findAgain = resolveFindAgainShortcut(e);\n if (findAgain !== undefined) {\n e.preventDefault();\n findNextMatch(findAgain === 'previous', true);\n }\n },\n });\n\n this.#inputElement = h('input', {\n type: 'text',\n placeholder: 'Search',\n dataset: 'search',\n value: defaultQuery,\n oninput: (e: Event) => {\n searchParams.text = (e.target as HTMLInputElement).value;\n matches.current = undefined;\n updateMatches();\n },\n onkeydown: (e: KeyboardEvent) => {\n const findAgain = resolveFindAgainShortcut(e);\n if (e.key === 'Escape') {\n e.preventDefault();\n close();\n } else if (e.key === 'Enter') {\n e.preventDefault();\n findNextMatch(false, true);\n } else if (findAgain !== undefined) {\n // Override the native browser find-again shortcut so cmd+g steps to\n // the next match and cmd+shift+g to the previous one.\n e.preventDefault();\n findNextMatch(findAgain === 'previous', true);\n } else if (\n isPrimaryModifier(e) &&\n (e.key === 'f' || e.code === 'KeyF')\n ) {\n // Prevent the default browser search panel and switch the panel mode\n // in place (cmd+f -> find, cmd+opt+f -> find/replace).\n e.preventDefault();\n applyMode(e.altKey ? 'replace' : 'find');\n }\n },\n });\n\n // The three search-option toggles are overlaid on the right edge of the\n // find input instead of sitting beside it.\n const searchTogglesElement = h('div', {\n dataset: 'searchToggles',\n children: [caseSensitiveToggle, wholeWordToggle, regexToggle],\n });\n\n const findInputBox = h('div', {\n dataset: { inputBox: '', find: '' },\n children: [this.#inputElement, searchTogglesElement],\n });\n\n // The replace input and its action buttons are tagged as replace cells so\n // they can be hidden together when the panel is in find-only mode.\n const replaceInputBox = h('div', {\n dataset: { inputBox: '', replace: '', replaceCell: '' },\n children: [replaceInputElement],\n });\n\n const replaceActionsElement = h('div', {\n dataset: { replaceActions: '', replaceCell: '' },\n children: [\n iconButton({ icon: 'replace', label: 'Replace', onClick: replace }),\n iconButton({\n icon: 'replace-all',\n label: 'Replace All',\n onClick: replaceAll,\n }),\n ],\n });\n\n // Held so the no-results state can toggle their native disabled flag.\n const prevButton = iconButton({\n icon: 'arrow-up',\n label: 'Previous',\n onClick: () => {\n findNextMatch(true);\n },\n });\n const nextButton = iconButton({\n icon: 'arrow-down',\n label: 'Next',\n onClick: () => {\n findNextMatch();\n },\n });\n prevButton.disabled = true;\n nextButton.disabled = true;\n\n const navElement = h('div', {\n dataset: 'searchNav',\n children: [prevButton, nextButton],\n });\n\n const closeElement = iconButton({\n icon: 'close',\n label: 'Close',\n onClick: close,\n dataset: { searchClose: '' },\n });\n\n // Cells are positioned by CSS grid-template-areas (see editor.css), so DOM\n // order here only drives tab/reading order, not layout. Keep the replace\n // input directly after the find input (and its toggles) so Tab walks\n // find -> replace before reaching the nav arrows and close button.\n const gridElement = h('div', {\n dataset: { searchGrid: '', mode },\n children: [\n findInputBox,\n replaceInputBox,\n replaceActionsElement,\n matchResultElement,\n navElement,\n closeElement,\n ],\n });\n\n // Toggles the panel between find and find/replace modes by showing or\n // hiding the replace cells, then returns focus to the find input.\n const applyMode = (next: SearchPanelMode) => {\n gridElement.dataset.mode = next;\n this.#inputElement.focus();\n this.#inputElement.select();\n };\n this.#applyMode = applyMode;\n\n this.#container = h('div', {\n dataset: 'searchPanel',\n children: [\n h('div', {\n dataset: 'editorWidget',\n children: [gridElement],\n }),\n ],\n });\n\n matches.current = initialMatch;\n containerElement.before(this.#container);\n\n requestAnimationFrame(() => {\n if (initialMatch !== undefined) {\n updateMatches();\n } else {\n onUpdate([]);\n }\n this.#inputElement.select();\n });\n }\n\n focus(): void {\n this.#inputElement.focus();\n }\n\n navigate(findPrevious: boolean): void {\n this.#navigate?.(findPrevious);\n }\n\n updateMatches(options?: { syncSelection?: boolean }): void {\n this.#updateMatches?.(options);\n }\n\n setMode(mode: SearchPanelMode): void {\n this.#applyMode?.(mode);\n }\n\n // Tears the panel down the way the close button and Escape-in-input do:\n // removes the DOM and runs onClose so the editor clears its match highlights.\n // Prefer this over cleanup() for any user-initiated dismissal.\n close(): void {\n this.#close?.();\n }\n\n // DOM-only teardown. Does NOT run onClose, so it leaves the editor's match\n // highlights in place; only use it when the caller clears that state itself\n // (e.g. a full editor reset). User-initiated dismissals should call close().\n cleanup(): void {\n this.#container.remove();\n }\n}\n"],"mappings":";;;;;;AAkCA,IAAa,oBAAb,MAA+B;CAC7B;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,SAA6B;EACvC,MAAM,EACJ,cACA,kBACA,cACA,OAAO,QACP,cACA,eACA,cACA,UACA,YACE;EAEJ,MAAM,eAA6B;GACjC,MAAM;GACN,aAAa;GACb,eAAe;GACf,WAAW;GACX,OAAO;EACT;EAEA,MAAM,UAAU;GACd,KAAK,CAAC;GACN,SAAS,KAAA;EACX;EAIA,MAAM,qBAAqB,EAAE,OAAO;GAClC,SAAS;IAAE,SAAS;IAAI,WAAW;GAAG;GACtC,aAAa;EACf,CAAC;EACD,MAAM,iBAAiB,YAA0C;GAC/D,QAAQ,MACN,aAAa,SAAS,KAAK,aAAa,OAAO,YAAY,IAAI,CAAC;GAClE,MAAM,YAAY,QAAQ,IAAI,WAAW;GACzC,WAAW,WAAW;GACtB,WAAW,WAAW;GAKtB,IAAI,WAAW;IACb,mBAAmB,cAAc;IACjC,mBAAmB,QAAQ,YAAY;IACvC,QAAQ,UAAU,KAAA;IAClB,SAAS,CAAC,CAAC;IACX;GACF;GAEA,OAAO,mBAAmB,QAAQ;GAClC,IAAI,SAAS,kBAAkB,OAAO;IAEpC,mBADqB,SAAS,QAAQ,KAAK,EAAE,eAAe,MAAM,CACpC,CAAC;IAC/B;GACF;GAEA,mBAAmB,SAAS,QAAQ,GAAG,CAAC;EAC1C;EACA,KAAKA,iBAAiB;EAEtB,MAAM,sBAAsB,iBAAyC;GACnE,IAAI,iBAAiB,KAAA,GACnB,mBAAmB,cAAc,GAAG,QAAQ,IAAI,OAAO;QAClD;IACL,MAAM,CAAC,OAAO,OAAO;IAIrB,mBAAmB,cAAc,GAHnB,QAAQ,IAAI,WACvB,MAAM,EAAE,OAAO,SAAS,EAAE,OAAO,GAEI,IAAI,EAAE,MAAM,QAAQ,IAAI;GAClE;GACA,QAAQ,UAAU;EACpB;EAEA,MAAM,qBACJ,KACA,UACG;GACH,aAAa,OAAO;GACpB,cAAc;EAChB;EAEA,MAAM,iBACJ,eAAwB,OACxB,cAAuB,UACpB;GACH,MAAM,aAAa,QAAQ;GAC3B,IAAI,YAAoC,WAAW;GACnD,IAAI,WAAW,SAAS,GACtB,IAAI,cAAc;IAChB,MAAM,eAAe,QAAQ,UAAU,MAAM;IAC7C,YAAY,WAAW,GAAG,EAAE;IAC5B,KAAK,MAAM,KAAK,YACd,IAAI,EAAE,MAAM,cACV,YAAY;SAEZ;GAGN,OAAO;IACL,MAAM,eAAe,QAAQ,UAAU,MAAM;IAC7C,KAAK,MAAM,KAAK,YACd,IAAI,EAAE,MAAM,cAAc;KACxB,YAAY;KACZ;IACF;GAEJ;GAEF,IAAI,cAAc,KAAA,GAAW;IAC3B,mBAAmB,SAAS;IAC5B,cAAc,WAAW,WAAW;GACtC;GACA,QAAQ,UAAU;EACpB;EAEA,KAAKC,aAAa,iBAChB,cAAc,cAAc,IAAI;EAElC,MAAM,wBACJ,YACA,cACsB;GACtB,OAAO;GACP,KAAK;GACL,MAAM,4BACH,WAAW,aAAa,WAAW,MAAM,IACzC,aAAa,aAAa,SAAS,QAAQ,IAC3C,SAAS,aAAa,YAAY,IAAI,GACvC,cACA,YACA,QACF;EACF;EAEA,MAAM,gBAAgB;GACpB,IAAI,aAAa,SAAS,MAAM,QAAQ,IAAI,WAAW,GACrD;GAGF,IAAI,eAAe,QAAQ;GAC3B,IAAI,iBAAiB,KAAA,GAAW;IAC9B,cAAc,OAAO,IAAI;IACzB,eAAe,QAAQ;IACvB,IAAI,iBAAiB,KAAA,GACnB;GAEJ;GAEA,MAAM,CAAC,OAAO,OAAO;GACrB,MAAM,OAAO,qBAAqB,OAAO,GAAG;GAC5C,aAAa,CAAC,IAAI,CAAC;GAGnB,cAAc,CAAC,QAAQ,KAAK,KAAK,QAAQ,QAAQ,KAAK,KAAK,MAAM,GAAG,IAAI;GACxE,QAAQ,UAAU,KAAA;GAClB,cAAc;EAChB;EAEA,MAAM,mBAAmB;GACvB,IAAI,aAAa,SAAS,MAAM,QAAQ,IAAI,WAAW,GACrD;GAGF,aACE,QAAQ,IAAI,KAAK,CAAC,OAAO,SAAS,qBAAqB,OAAO,GAAG,CAAC,CACpE;GACA,QAAQ,UAAU,KAAA;GAClB,cAAc;EAChB;EAEA,MAAM,cAAc;GAClB,KAAK,QAAQ;GACb,QAAQ;EACV;EACA,KAAKC,SAAS;EAMd,MAAM,cAAc,SAOlB,EAAE,UAAU;GACV,MAAM;GACN,OAAO,KAAK;GACZ,WAAW,KAAK;GAChB,SAAS;IAAE,YAAY;IAAI,GAAG,KAAK;GAAQ;GAC3C,WAAW,iBAAiB,KAAK,MAAM,KAAK,QAAQ,EAAE;GACtD,SAAS,KAAK;EAChB,CAAC;EAMH,MAAM,cACJ,MACA,OACA,QACG;GACH,MAAM,SAAS,WAAW;IACxB;IACA,OAAO;IACP,MAAM;IACN,eAAe;KACb,MAAM,OAAO,CAAC,aAAa;KAC3B,OAAO,cAAc,OAAO,IAAI;KAChC,kBAAkB,KAAK,IAAI;IAC7B;GACF,CAAC;GACD,OAAO,cAAc,OAAO,aAAa,IAAI;GAC7C,OAAO;EACT;EAEA,MAAM,sBAAsB,WAC1B,QACA,cACA,eACF;EACA,MAAM,kBAAkB,WAAW,cAAc,cAAc,WAAW;EAC1E,MAAM,cAAc,WAAW,SAAS,UAAU,OAAO;EAEzD,MAAM,sBAAsB,EAAE,SAAS;GACrC,MAAM;GACN,aAAa;GACb,SAAS;GACT,OAAO;GACP,UAAU,MAAa;IACrB,aAAa,cAAe,EAAE,OAA4B;GAC5D;GACA,YAAY,MAAqB;IAC/B,MAAM,YAAY,yBAAyB,CAAC;IAC5C,IAAI,cAAc,KAAA,GAAW;KAC3B,EAAE,eAAe;KACjB,cAAc,cAAc,YAAY,IAAI;IAC9C;GACF;EACF,CAAC;EAED,KAAKC,gBAAgB,EAAE,SAAS;GAC9B,MAAM;GACN,aAAa;GACb,SAAS;GACT,OAAO;GACP,UAAU,MAAa;IACrB,aAAa,OAAQ,EAAE,OAA4B;IACnD,QAAQ,UAAU,KAAA;IAClB,cAAc;GAChB;GACA,YAAY,MAAqB;IAC/B,MAAM,YAAY,yBAAyB,CAAC;IAC5C,IAAI,EAAE,QAAQ,UAAU;KACtB,EAAE,eAAe;KACjB,MAAM;IACR,OAAO,IAAI,EAAE,QAAQ,SAAS;KAC5B,EAAE,eAAe;KACjB,cAAc,OAAO,IAAI;IAC3B,OAAO,IAAI,cAAc,KAAA,GAAW;KAGlC,EAAE,eAAe;KACjB,cAAc,cAAc,YAAY,IAAI;IAC9C,OAAO,IACL,kBAAkB,CAAC,MAClB,EAAE,QAAQ,OAAO,EAAE,SAAS,SAC7B;KAGA,EAAE,eAAe;KACjB,UAAU,EAAE,SAAS,YAAY,MAAM;IACzC;GACF;EACF,CAAC;EAID,MAAM,uBAAuB,EAAE,OAAO;GACpC,SAAS;GACT,UAAU;IAAC;IAAqB;IAAiB;GAAW;EAC9D,CAAC;EAED,MAAM,eAAe,EAAE,OAAO;GAC5B,SAAS;IAAE,UAAU;IAAI,MAAM;GAAG;GAClC,UAAU,CAAC,KAAKA,eAAe,oBAAoB;EACrD,CAAC;EAID,MAAM,kBAAkB,EAAE,OAAO;GAC/B,SAAS;IAAE,UAAU;IAAI,SAAS;IAAI,aAAa;GAAG;GACtD,UAAU,CAAC,mBAAmB;EAChC,CAAC;EAED,MAAM,wBAAwB,EAAE,OAAO;GACrC,SAAS;IAAE,gBAAgB;IAAI,aAAa;GAAG;GAC/C,UAAU,CACR,WAAW;IAAE,MAAM;IAAW,OAAO;IAAW,SAAS;GAAQ,CAAC,GAClE,WAAW;IACT,MAAM;IACN,OAAO;IACP,SAAS;GACX,CAAC,CACH;EACF,CAAC;EAGD,MAAM,aAAa,WAAW;GAC5B,MAAM;GACN,OAAO;GACP,eAAe;IACb,cAAc,IAAI;GACpB;EACF,CAAC;EACD,MAAM,aAAa,WAAW;GAC5B,MAAM;GACN,OAAO;GACP,eAAe;IACb,cAAc;GAChB;EACF,CAAC;EACD,WAAW,WAAW;EACtB,WAAW,WAAW;EAEtB,MAAM,aAAa,EAAE,OAAO;GAC1B,SAAS;GACT,UAAU,CAAC,YAAY,UAAU;EACnC,CAAC;EAED,MAAM,eAAe,WAAW;GAC9B,MAAM;GACN,OAAO;GACP,SAAS;GACT,SAAS,EAAE,aAAa,GAAG;EAC7B,CAAC;EAMD,MAAM,cAAc,EAAE,OAAO;GAC3B,SAAS;IAAE,YAAY;IAAI;GAAK;GAChC,UAAU;IACR;IACA;IACA;IACA;IACA;IACA;GACF;EACF,CAAC;EAID,MAAM,aAAa,SAA0B;GAC3C,YAAY,QAAQ,OAAO;GAC3B,KAAKA,cAAc,MAAM;GACzB,KAAKA,cAAc,OAAO;EAC5B;EACA,KAAKC,aAAa;EAElB,KAAKC,aAAa,EAAE,OAAO;GACzB,SAAS;GACT,UAAU,CACR,EAAE,OAAO;IACP,SAAS;IACT,UAAU,CAAC,WAAW;GACxB,CAAC,CACH;EACF,CAAC;EAED,QAAQ,UAAU;EAClB,iBAAiB,OAAO,KAAKA,UAAU;EAEvC,4BAA4B;GAC1B,IAAI,iBAAiB,KAAA,GACnB,cAAc;QAEd,SAAS,CAAC,CAAC;GAEb,KAAKF,cAAc,OAAO;EAC5B,CAAC;CACH;CAEA,QAAc;EACZ,KAAKA,cAAc,MAAM;CAC3B;CAEA,SAAS,cAA6B;EACpC,KAAKF,YAAY,YAAY;CAC/B;CAEA,cAAc,SAA6C;EACzD,KAAKD,iBAAiB,OAAO;CAC/B;CAEA,QAAQ,MAA6B;EACnC,KAAKI,aAAa,IAAI;CACxB;CAKA,QAAc;EACZ,KAAKF,SAAS;CAChB;CAKA,UAAgB;EACd,KAAKG,WAAW,OAAO;CACzB;AACF"}
|
|
@@ -141,6 +141,18 @@ declare function extendSelections(selections: EditorSelection[], target: EditorS
|
|
|
141
141
|
* Merges overlapping selections.
|
|
142
142
|
*/
|
|
143
143
|
declare function mergeOverlappingSelections(selections: EditorSelection[]): EditorSelection[];
|
|
144
|
+
/**
|
|
145
|
+
* Converts selections into merged line blocks for line-based commands.
|
|
146
|
+
*/
|
|
147
|
+
declare function getSelectedLineBlocks(selections: EditorSelection[]): {
|
|
148
|
+
startLine: number;
|
|
149
|
+
endLine: number;
|
|
150
|
+
}[];
|
|
151
|
+
/**
|
|
152
|
+
* Moves a selection's line positions after its lines are shifted, clamping to
|
|
153
|
+
* the target document bounds.
|
|
154
|
+
*/
|
|
155
|
+
declare function shiftSelectionLines(selection: EditorSelection, direction: -1 | 1, lineCount: number, getLineLength: (line: number) => number): EditorSelection;
|
|
144
156
|
/**
|
|
145
157
|
* Finds the next matching word and updates the selections.
|
|
146
158
|
*/
|
|
@@ -175,5 +187,5 @@ declare function getSelectionAnchor(lineElement: HTMLElement, character: number)
|
|
|
175
187
|
*/
|
|
176
188
|
declare function expandCollapsedSelectionToWord(textDocument: TextDocument<unknown>, selection: EditorSelection): EditorSelection;
|
|
177
189
|
//#endregion
|
|
178
|
-
export { AutoSurround, DirectionBackward, DirectionForward, DirectionNone, EditorSelection, SelectionDirection, applyDeleteCharacterToSelections, applyDeleteHardLineForwardToSelections, applyDeleteSoftLineBackwardToSelections, applyDeleteWordBackwardToSelections, applyTextChangeToSelections, applyTextReplaceToSelections, applyTransposeToSelections, comparePosition, convertSelection, createSelectionFrom, createSelectionFromAnchorAndFocusOffsets, expandCollapsedSelectionToWord, extendSelection, extendSelections, findNexMatch, getAutoSurroundReplacementTexts, getCaretPosition, getDocumentBoundarySelection, getDocumentFullSelection, getSelectionAnchor, getSelectionText, isCollapsedSelection, isLineEditable, mapCursorMove, mapSelectionShift, mergeOverlappingSelections, remapSelectionsAfterEdits, resolveDeleteCharacterRange, resolveIndentEdits, resolveSelectionCut, selectionIntersects };
|
|
190
|
+
export { AutoSurround, DirectionBackward, DirectionForward, DirectionNone, EditorSelection, SelectionDirection, applyDeleteCharacterToSelections, applyDeleteHardLineForwardToSelections, applyDeleteSoftLineBackwardToSelections, applyDeleteWordBackwardToSelections, applyTextChangeToSelections, applyTextReplaceToSelections, applyTransposeToSelections, comparePosition, convertSelection, createSelectionFrom, createSelectionFromAnchorAndFocusOffsets, expandCollapsedSelectionToWord, extendSelection, extendSelections, findNexMatch, getAutoSurroundReplacementTexts, getCaretPosition, getDocumentBoundarySelection, getDocumentFullSelection, getSelectedLineBlocks, getSelectionAnchor, getSelectionText, isCollapsedSelection, isLineEditable, mapCursorMove, mapSelectionShift, mergeOverlappingSelections, remapSelectionsAfterEdits, resolveDeleteCharacterRange, resolveIndentEdits, resolveSelectionCut, selectionIntersects, shiftSelectionLines };
|
|
179
191
|
//# sourceMappingURL=selection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selection.d.ts","names":[],"sources":["../../src/editor/selection.ts"],"mappings":";;;;cAgBa,iBAAA;AAAA,cACA,aAAA;AAAA,cACA,gBAAA;AAAA,KAED,kBAAA,UACD,iBAAA,UACA,aAAA,UACA,gBAAA;AAAA,UAEM,eAAA,SAAwB,KAAK;EAC5C,SAAA,EAAW,kBAAA;AAAA;AATb;;;AAAA,iBAegB,gBAAA,CACd,KAAA,EAAO,WAAA,EACP,SAAA,GAAW,kBAAA,GACV,eAAA;AAlBuB;AAC1B;;AAD0B,iBAkCV,kBAAA,CACd,YAAA,EAAc,YAAA,WACd,SAAA,EAAW,eAAA,EACX,OAAA,UACA,OAAA,aACE,KAAA,EAAO,QAAA,IAAY,aAAA,EAAe,eAAA;;AAtCT;AAE7B;iBAqGgB,aAAA,CACd,YAAA,EAAc,YAAA,WACd,UAAA,EAAY,eAAA,IACZ,QAAA,qEACC,eAAA;;;;iBAkEa,iBAAA,CACd,YAAA,EAAc,YAAA,WACd,UAAA,EAAY,eAAA,IACZ,QAAA,qEACC,eAAA;;;;iBAwBa,2BAAA,cACd,YAAA,EAAc,YAAA,CAAa,WAAA,GAC3B,UAAA,EAAY,eAAA,IACZ,IAAA,EAAM,gBAAA,EACN,eAAA,GAAkB,kBAAA,CAAmB,WAAA,KACrC,OAAA,WACA,YAAA;EAEA,cAAA,EAAgB,eAAA;EAChB,MAAA,GAAS,kBAAA;AAAA;AA3MX;;;AAAA,iBAsYgB,4BAAA,cACd,YAAA,EAAc,YAAA,CAAa,WAAA,GAC3B,UAAA,EAAY,eAAA,IACZ,KAAA,YACA,eAAA,GAAkB,kBAAA,CAAmB,WAAA,KACrC,YAAA;EAEA,cAAA,EAAgB,eAAA;EAChB,MAAA,GAAS,kBAAA;AAAA;AAAA,KAiLC,YAAA;AA9jBmB;AAM/B;;;AAN+B,iBAylBf,+BAAA,cACd,YAAA,EAAc,YAAA,CAAa,WAAA,GAC3B,UAAA,EAAY,eAAA,IACZ,IAAA,UACA,YAAA,GAAe,YAAA;;;;;iBAuBD,0BAAA,cACd,YAAA,EAAc,YAAA,CAAa,WAAA,GAC3B,UAAA,EAAY,eAAA,IACZ,eAAA,GAAkB,kBAAA,CAAmB,WAAA;EAErC,cAAA,EAAgB,eAAA;EAChB,MAAA,GAAS,kBAAA;AAAA;;;AAjnBO;AAgBlB;;
|
|
1
|
+
{"version":3,"file":"selection.d.ts","names":[],"sources":["../../src/editor/selection.ts"],"mappings":"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"}
|
package/dist/editor/selection.js
CHANGED
|
@@ -222,7 +222,7 @@ function getNextSelectionOffsetPairAfterReplace(textDocument, entry, offsetDelta
|
|
|
222
222
|
const insertStart = entry.start + offsetDelta;
|
|
223
223
|
const insertEnd = insertStart + newText.length;
|
|
224
224
|
if (entry.end - entry.start > 0) {
|
|
225
|
-
const originalText = textDocument.
|
|
225
|
+
const originalText = textDocument.getTextSlice(entry.start, entry.end);
|
|
226
226
|
const preservedOffset = newText.indexOf(originalText);
|
|
227
227
|
if (preservedOffset !== -1 && preservedOffset + originalText.length <= newText.length) {
|
|
228
228
|
const rangeStart = insertStart + preservedOffset;
|
|
@@ -376,7 +376,6 @@ function getAutoSurroundReplacementTexts(textDocument, selections, char, autoSur
|
|
|
376
376
|
* insertTranspose (Ctrl+T) behavior.
|
|
377
377
|
*/
|
|
378
378
|
function applyTransposeToSelections(textDocument, selections, lineAnnotations) {
|
|
379
|
-
const text = textDocument.getText();
|
|
380
379
|
const edits = [];
|
|
381
380
|
const nextOffsetPairs = [];
|
|
382
381
|
for (const selection of selections) {
|
|
@@ -421,7 +420,7 @@ function applyTransposeToSelections(textDocument, selections, lineAnnotations) {
|
|
|
421
420
|
const prevGraphemeStart = prevLength > 0 ? findClusterBreak(prevLineText, prevLength, false, getLineGraphemeStarts(prevLineText)) : prevLength;
|
|
422
421
|
const firstEnd = findClusterBreak(lineText, 0, true, graphemeStarts);
|
|
423
422
|
const prevStart = prevEnd - (prevLength - prevGraphemeStart);
|
|
424
|
-
const newText = lineText.slice(0, firstEnd) +
|
|
423
|
+
const newText = lineText.slice(0, firstEnd) + textDocument.getTextSlice(prevEnd, offset) + prevLineText.slice(prevGraphemeStart, prevLength);
|
|
425
424
|
edit = {
|
|
426
425
|
start: prevStart,
|
|
427
426
|
end: offset + firstEnd,
|
|
@@ -774,6 +773,58 @@ function mergeOverlappingSelections(selections) {
|
|
|
774
773
|
return merged.sort((a, b) => a.index - b.index).map(({ selection }) => selection);
|
|
775
774
|
}
|
|
776
775
|
/**
|
|
776
|
+
* Converts selections into merged line blocks for line-based commands.
|
|
777
|
+
*/
|
|
778
|
+
function getSelectedLineBlocks(selections) {
|
|
779
|
+
const blocks = selections.map((selection) => {
|
|
780
|
+
let endLine = selection.end.line;
|
|
781
|
+
if (selection.end.character === 0 && comparePosition(selection.start, selection.end) !== 0) endLine--;
|
|
782
|
+
return {
|
|
783
|
+
startLine: selection.start.line,
|
|
784
|
+
endLine: Math.max(selection.start.line, endLine)
|
|
785
|
+
};
|
|
786
|
+
}).sort((a, b) => {
|
|
787
|
+
const startOrder = a.startLine - b.startLine;
|
|
788
|
+
return startOrder !== 0 ? startOrder : a.endLine - b.endLine;
|
|
789
|
+
});
|
|
790
|
+
const merged = [];
|
|
791
|
+
for (const block of blocks) {
|
|
792
|
+
const previous = merged.at(-1);
|
|
793
|
+
if (previous !== void 0 && block.startLine <= previous.endLine + 1) previous.endLine = Math.max(previous.endLine, block.endLine);
|
|
794
|
+
else merged.push({ ...block });
|
|
795
|
+
}
|
|
796
|
+
return merged;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Moves a selection's line positions after its lines are shifted, clamping to
|
|
800
|
+
* the target document bounds.
|
|
801
|
+
*/
|
|
802
|
+
function shiftSelectionLines(selection, direction, lineCount, getLineLength) {
|
|
803
|
+
const shiftPosition = (position) => {
|
|
804
|
+
const line = position.line + direction;
|
|
805
|
+
if (line >= lineCount) {
|
|
806
|
+
const lastLine = Math.max(0, lineCount - 1);
|
|
807
|
+
return {
|
|
808
|
+
line: lastLine,
|
|
809
|
+
character: getLineLength(lastLine)
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
if (line < 0) return {
|
|
813
|
+
line: 0,
|
|
814
|
+
character: 0
|
|
815
|
+
};
|
|
816
|
+
return {
|
|
817
|
+
line,
|
|
818
|
+
character: position.character
|
|
819
|
+
};
|
|
820
|
+
};
|
|
821
|
+
return {
|
|
822
|
+
start: shiftPosition(selection.start),
|
|
823
|
+
end: shiftPosition(selection.end),
|
|
824
|
+
direction: selection.direction
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
777
828
|
* Finds the next matching word and updates the selections.
|
|
778
829
|
*/
|
|
779
830
|
function findNexMatch(textDocument, selections) {
|
|
@@ -1442,6 +1493,6 @@ function getTextOffset(text, offset) {
|
|
|
1442
1493
|
return Math.min(offset, lineBreakIndex === -1 ? value.length : lineBreakIndex);
|
|
1443
1494
|
}
|
|
1444
1495
|
//#endregion
|
|
1445
|
-
export { DirectionBackward, DirectionForward, DirectionNone, applyDeleteCharacterToSelections, applyDeleteHardLineForwardToSelections, applyDeleteSoftLineBackwardToSelections, applyDeleteWordBackwardToSelections, applyTextChangeToSelections, applyTextReplaceToSelections, applyTransposeToSelections, comparePosition, convertSelection, createSelectionFrom, createSelectionFromAnchorAndFocusOffsets, expandCollapsedSelectionToWord, extendSelection, extendSelections, findNexMatch, getAutoSurroundReplacementTexts, getCaretPosition, getDocumentBoundarySelection, getDocumentFullSelection, getSelectionAnchor, getSelectionText, isCollapsedSelection, isLineEditable, mapCursorMove, mapSelectionShift, mergeOverlappingSelections, remapSelectionsAfterEdits, resolveDeleteCharacterRange, resolveIndentEdits, resolveSelectionCut, selectionIntersects };
|
|
1496
|
+
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, getSelectionText, isCollapsedSelection, isLineEditable, mapCursorMove, mapSelectionShift, mergeOverlappingSelections, remapSelectionsAfterEdits, resolveDeleteCharacterRange, resolveIndentEdits, resolveSelectionCut, selectionIntersects, shiftSelectionLines };
|
|
1446
1497
|
|
|
1447
1498
|
//# sourceMappingURL=selection.js.map
|