diff --git a/media/editor/dataDisplay.tsx b/media/editor/dataDisplay.tsx index 53a8c41f..9857b636 100644 --- a/media/editor/dataDisplay.tsx +++ b/media/editor/dataDisplay.tsx @@ -4,14 +4,15 @@ import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRecoilValue, useSetRecoilState } from "recoil"; import { EditRangeOp, HexDocumentEditOp } from "../../shared/hexDocumentModel"; -import { InspectorLocation, MessageType } from "../../shared/protocol"; +import { DeleteAcceptedMessage, InspectorLocation, MessageType } from "../../shared/protocol"; +import { Range } from "../../shared/util/range"; import { PastePopup } from "./copyPaste"; import _style from "./dataDisplay.css"; import { FocusedElement, dataCellCls, useDisplayContext, useIsFocused, useIsHovered, useIsSelected, useIsUnsaved } from "./dataDisplayContext"; import { DataInspectorAside } from "./dataInspector"; import { useGlobalHandler, useLastAsyncRecoilValue } from "./hooks"; import * as select from "./state"; -import { Range, clamp, clsx, getAsciiCharacter, getScrollDimensions, throwOnUndefinedAccessInDev } from "./util"; +import { clamp, clsx, getAsciiCharacter, getScrollDimensions, throwOnUndefinedAccessInDev } from "./util"; const style = throwOnUndefinedAccessInDev(_style); @@ -457,6 +458,17 @@ const DataCell: React.FC<{ return; } + if (e.key === "Backspace" || e.key === "Delete") { + // this is a bit of a hack, but this is kind of tricky: we got a delete + // for a range, and the edit must be undoable, but we aren't ensured to + // have the data paged in for the range. So make a separate request + // that will result in the extension host sending the edit to us. + select.messageHandler.sendRequest({ + type: MessageType.RequestDeletes, + deletes: ctx.getSelectionRanges().map(r => ({ start: r.start, end: r.end })), + }).then(() => ctx.setSelectionRanges([])); + } + let newValue: number; if (isChar && e.key.length === 1) { newValue = e.key.charCodeAt(0); diff --git a/media/editor/dataDisplayContext.tsx b/media/editor/dataDisplayContext.tsx index 732f2ea2..08f63024 100644 --- a/media/editor/dataDisplayContext.tsx +++ b/media/editor/dataDisplayContext.tsx @@ -3,9 +3,10 @@ import { createContext, useContext, useEffect, useState } from "react"; import { SetterOrUpdater } from "recoil"; import { HexDocumentEdit } from "../../shared/hexDocumentModel"; import { MessageType } from "../../shared/protocol"; +import { Range, getRangeSelectionsFromStack } from "../../shared/util/range"; import _style from "./dataDisplayContext.css"; import { messageHandler, registerHandler } from "./state"; -import { Range, throwOnUndefinedAccessInDev } from "./util"; +import { throwOnUndefinedAccessInDev } from "./util"; const style = throwOnUndefinedAccessInDev(_style); @@ -253,24 +254,25 @@ export class DisplayContext { } private publishSelections() { - const selectedBytes = new Set(); - for (const range of this._selection) { - for (let i = range.start; i < range.end; i++) { - if (selectedBytes.has(i)) { - selectedBytes.delete(i); - } else { - selectedBytes.add(i); - } - } + let selected = 0; + for (const range of this.getSelectionRanges()) { + selected += range.size; } messageHandler.sendEvent({ type: MessageType.SetSelectedCount, - selected: selectedBytes.size, + selected: selected, focused: this._focusedByte?.byte }); } + /** + * Gets selected ranges. + */ + public getSelectionRanges(): Range[] { + return getRangeSelectionsFromStack(this._selection); + } + /** * Replaces the selection with the given ranges. */ diff --git a/media/editor/findWidget.tsx b/media/editor/findWidget.tsx index 614a6849..f306d095 100644 --- a/media/editor/findWidget.tsx +++ b/media/editor/findWidget.tsx @@ -13,11 +13,12 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { useRecoilState, useRecoilValue } from "recoil"; import { HexDocumentEditOp, HexDocumentReplaceEdit } from "../../shared/hexDocumentModel"; import { LiteralSearchQuery, MessageType, SearchRequestMessage, SearchResult, SearchResultsWithProgress } from "../../shared/protocol"; +import { Range } from "../../shared/util/range"; import { FocusedElement, dataCellCls, useDisplayContext } from "./dataDisplayContext"; import _style from "./findWidget.css"; import { usePersistedState } from "./hooks"; import * as select from "./state"; -import { Range, clsx, hexDecode, isHexString, parseHexDigit, throwOnUndefinedAccessInDev } from "./util"; +import { clsx, hexDecode, isHexString, parseHexDigit, throwOnUndefinedAccessInDev } from "./util"; import { VsIconButton, VsIconCheckbox, VsProgressIndicator, VsTextFieldGroup } from "./vscodeUi"; const style = throwOnUndefinedAccessInDev(_style); diff --git a/media/editor/scrollContainer.tsx b/media/editor/scrollContainer.tsx index c63ff355..d12af415 100644 --- a/media/editor/scrollContainer.tsx +++ b/media/editor/scrollContainer.tsx @@ -1,9 +1,10 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { useRecoilState, useRecoilValue } from "recoil"; +import { Range } from "../../shared/util/range"; import { DataDisplay } from "./dataDisplay"; import _style from "./scrollContainer.css"; import * as select from "./state"; -import { Range, throwOnUndefinedAccessInDev } from "./util"; +import { throwOnUndefinedAccessInDev } from "./util"; import { VirtualScrollContainer } from "./virtualScrollContainer"; const style = throwOnUndefinedAccessInDev(_style); diff --git a/media/editor/state.ts b/media/editor/state.ts index d4e23864..e257b6ab 100644 --- a/media/editor/state.ts +++ b/media/editor/state.ts @@ -6,7 +6,8 @@ import { atom, DefaultValue, selector, selectorFamily } from "recoil"; import { buildEditTimeline, HexDocumentEdit, readUsingRanges } from "../../shared/hexDocumentModel"; import { FromWebviewMessage, InspectorLocation, MessageHandler, MessageType, ReadRangeResponseMessage, ReadyResponseMessage, SearchResultsWithProgress, ToWebviewMessage } from "../../shared/protocol"; import { deserializeEdits, serializeEdits } from "../../shared/serialization"; -import { clamp, Range } from "./util"; +import { Range } from "../../shared/util/range"; +import { clamp } from "./util"; const acquireVsCodeApi: () => ({ postMessage(msg: unknown): void; @@ -290,7 +291,8 @@ export const edits = atom({ }); registerHandler(MessageType.SetEdits, msg => { - fx.setSelf(deserializeEdits(msg.edits)); + const edits = deserializeEdits(msg.edits); + fx.setSelf(prev => msg.appendOnly ? [...(prev instanceof DefaultValue ? [] : prev), ...edits] : edits); }); } ] diff --git a/media/editor/util.ts b/media/editor/util.ts index ec310351..12e6f76c 100644 --- a/media/editor/util.ts +++ b/media/editor/util.ts @@ -3,6 +3,7 @@ // Assorted helper functions +import { Range } from "../../shared/util/range"; import _style from "./util.css"; /** @@ -42,116 +43,6 @@ export const clsx = (...classes: (string | false | undefined | null)[]): string return out; }; -/** Direction for the {@link Range} */ -export const enum RangeDirection { - /** When the range was constructed, end >= start */ - Ascending, - /** When the range was constructed, start > end */ - Descending, -} - -/** - * @description Class which represents a range of numbers. Ranges represent - * a number range [start, end). They may be directional, as indicated by - * the order of arguments in the constructor and reflected in the {@link direction}. - */ -export class Range { - public readonly direction: RangeDirection; - /** - * Gets the number of integers in the range [start, end) - */ - public get size(): number { - return this.end - this.start; - } - - /** - * Returns a range containing the single byte. - */ - public static single(byte: number): Range { - return new Range(byte, byte + 1); - } - - /** - * Creates a new range representing [start, end], inclusive. - */ - public static inclusive(start: number, end: number): Range { - return end >= start ? new Range(start, end + 1) : new Range(start + 1, end); - } - - /** - * @description Constructs a range object representing [start, end) - * @param start Represents the start of the range - * @param end Represents the end of the range - * @param direction The direction of the range, inferred from - * argument order if not provided. - */ - constructor( - public readonly start: number, - public readonly end: number = Number.MAX_SAFE_INTEGER, - direction?: RangeDirection, - ) { - if (start < 0) { - throw new Error("Cannot construct a range with a negative start"); - } - - if (end < start) { - [this.start, this.end] = [end, start]; - direction ??= RangeDirection.Descending; - } else { - direction ??= RangeDirection.Ascending; - } - - this.direction = direction; - } - /** - * @desciption Tests if the given number if within the range - * @param {number} num The number to test - * @returns {boolean} True if the number is in the range, false otherwise - */ - public includes(num: number): boolean { - return num >= this.start && num < this.end; - } - - /** - * Expands the range to include the given value, if it is not already - * within the range. - */ - public expandToContain(value: number): Range { - if (value < this.start) { - return new Range(value, this.end, this.direction); - } else if (value >= this.end) { - return new Range(this.start, value + 1, this.direction); - } else { - return this; - } - } - /** - * Returns whether this range overlaps the other one. - */ - public overlaps(other: Range): boolean { - return other.end > this.start && other.start < this.end; - } - /** - * Returns one or more ranges representing ranges covered by exactly one of - * this or the `otherRange`. - */ - public difference(otherRange: Range): Range[] { - if (!this.overlaps(otherRange)) { - return [this, otherRange]; - } - - const delta: Range[] = []; - if (this.start !== otherRange.start) { - delta.push(new Range(otherRange.start, this.start)); - } - if (this.end !== otherRange.end) { - delta.push(new Range(otherRange.end, this.end)); - } - - return delta; - } -} - /** * @description Checks if the given number is in any of the ranges * @param {number} num The number to use when checking the ranges diff --git a/package-lock.json b/package-lock.json index ffda630c..5e1e174f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@types/vscode": "^1.74.0", "@typescript-eslint/eslint-plugin": "^5.9.1", "@typescript-eslint/parser": "^5.9.1", - "@vscode/test-electron": "^2.0.3", + "@vscode/test-electron": "^2.3.9", "chai": "^4.3.4", "esbuild": "^0.15.10", "esbuild-css-modules-plugin": "^2.7.1", @@ -1216,18 +1216,33 @@ } }, "node_modules/@vscode/test-electron": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.0.3.tgz", - "integrity": "sha512-1QXF3hW2saWk01XRqh5CMtzdKZAfM27UO7eYnnx6gaqxwD1+TP4T5s8ACDgK5e9qvzOdMBMy75IVpJudBrDz4w==", + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.9.tgz", + "integrity": "sha512-z3eiChaCQXMqBnk2aHHSEkobmC2VRalFQN0ApOAtydL172zXGxTwGrRtviT5HnUB+Q+G3vtEYFtuQkYqBzYgMA==", "dev": true, "dependencies": { "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "jszip": "^3.10.1", + "semver": "^7.5.2" }, "engines": { - "node": ">=8.9.3" + "node": ">=16" + } + }, + "node_modules/@vscode/test-electron/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/acorn": { @@ -1346,28 +1361,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1377,12 +1370,6 @@ "node": ">=8" } }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1439,24 +1426,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true, - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/camelcase": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", @@ -1502,18 +1471,6 @@ "node": ">=4" } }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1743,15 +1700,6 @@ "node": ">=6.0.0" } }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, "node_modules/electron-to-chromium": { "version": "1.4.274", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.274.tgz", @@ -2722,33 +2670,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -2938,6 +2859,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3044,7 +2971,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true }, "node_modules/isexe": { @@ -3117,6 +3044,18 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3130,6 +3069,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.19.0.tgz", @@ -3322,12 +3270,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, "node_modules/loader-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", @@ -3513,24 +3455,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mocha": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", @@ -3741,6 +3665,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4073,9 +4003,9 @@ } }, "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "dependencies": { "core-util-is": "~1.0.0", @@ -4222,7 +4152,7 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, "node_modules/shebang-command": { @@ -4374,15 +4304,6 @@ "node": ">=8.0" } }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -4465,24 +4386,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -5513,15 +5416,26 @@ } }, "@vscode/test-electron": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.0.3.tgz", - "integrity": "sha512-1QXF3hW2saWk01XRqh5CMtzdKZAfM27UO7eYnnx6gaqxwD1+TP4T5s8ACDgK5e9qvzOdMBMy75IVpJudBrDz4w==", + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.9.tgz", + "integrity": "sha512-z3eiChaCQXMqBnk2aHHSEkobmC2VRalFQN0ApOAtydL172zXGxTwGrRtviT5HnUB+Q+G3vtEYFtuQkYqBzYgMA==", "dev": true, "requires": { "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "jszip": "^3.10.1", + "semver": "^7.5.2" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "acorn": { @@ -5607,34 +5521,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "dev": true - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5672,18 +5564,6 @@ "update-browserslist-db": "^1.0.9" } }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, "camelcase": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", @@ -5710,15 +5590,6 @@ "type-detect": "^4.0.5" } }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5887,15 +5758,6 @@ "esutils": "^2.0.2" } }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, "electron-to-chromium": { "version": "1.4.274", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.274.tgz", @@ -6524,29 +6386,6 @@ "dev": true, "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -6686,6 +6525,12 @@ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6765,7 +6610,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true }, "isexe": { @@ -6824,6 +6669,18 @@ "universalify": "^2.0.0" } }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6834,6 +6691,15 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, "lightningcss": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.19.0.tgz", @@ -6913,12 +6779,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, "loader-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", @@ -7055,21 +6915,6 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, "mocha": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", @@ -7226,6 +7071,12 @@ "p-limit": "^3.0.2" } }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7443,9 +7294,9 @@ } }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -7542,7 +7393,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, "shebang-command": { @@ -7658,12 +7509,6 @@ "is-number": "^7.0.0" } }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -7720,24 +7565,6 @@ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, "update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", diff --git a/package.json b/package.json index 946caa1d..7c1e12ba 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "@types/vscode": "^1.74.0", "@typescript-eslint/eslint-plugin": "^5.9.1", "@typescript-eslint/parser": "^5.9.1", - "@vscode/test-electron": "^2.0.3", + "@vscode/test-electron": "^2.3.9", "chai": "^4.3.4", "esbuild": "^0.15.10", "esbuild-css-modules-plugin": "^2.7.1", diff --git a/shared/protocol.ts b/shared/protocol.ts index 2884b9aa..01b1f248 100644 --- a/shared/protocol.ts +++ b/shared/protocol.ts @@ -18,12 +18,14 @@ export const enum MessageType { SetFocusedByteRange, SetSelectedCount, PopDisplayedOffset, + DeleteAccepted, //#endregion //#region from webview ReadyRequest, OpenDocument, ReadRangeRequest, MakeEdits, + RequestDeletes, SearchRequest, CancelSearch, ClearDataInspector, @@ -113,6 +115,7 @@ export interface SetEditsMessage { type: MessageType.SetEdits; edits: ISerializedEdits; replaceFileSize?: number | null; + appendOnly?: boolean; } /** Sets the displayed offset. */ @@ -151,6 +154,11 @@ export interface PopDisplayedOffsetMessage { type: MessageType.PopDisplayedOffset; } +/** Acks a deletion request. */ +export interface DeleteAcceptedMessage { + type: MessageType.DeleteAccepted; +} + export type ToWebviewMessage = | ReadyResponseMessage | ReadRangeResponseMessage @@ -162,7 +170,8 @@ export type ToWebviewMessage = | SetFocusedByteMessage | SetFocusedByteRangeMessage | PopDisplayedOffsetMessage - | StashDisplayedOffsetMessage; + | StashDisplayedOffsetMessage + | DeleteAcceptedMessage; export interface OpenDocumentMessage { type: MessageType.OpenDocument; @@ -230,6 +239,11 @@ export interface CopyMessage { asText: boolean; } +export interface RequestDeletesMessage { + type: MessageType.RequestDeletes; + deletes: { start: number; end: number }[]; +} + export type FromWebviewMessage = | OpenDocumentMessage | ReadRangeMessage @@ -242,7 +256,8 @@ export type FromWebviewMessage = | ReadyRequestMessage | UpdateEditorSettings | PasteMessage - | CopyMessage; + | CopyMessage + | RequestDeletesMessage; export type ExtensionHostMessageHandler = MessageHandler; export type WebviewMessageHandler = MessageHandler; diff --git a/shared/util/range.ts b/shared/util/range.ts new file mode 100644 index 00000000..d4cec4fc --- /dev/null +++ b/shared/util/range.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +/** Direction for the {@link Range} */ +export const enum RangeDirection { + /** When the range was constructed, end >= start */ + Ascending, + /** When the range was constructed, start > end */ + Descending, +} + +/** + * @description Class which represents a range of numbers. Ranges represent + * a number range [start, end). They may be directional, as indicated by + * the order of arguments in the constructor and reflected in the {@link direction}. + */ + +export class Range { + public readonly direction: RangeDirection; + /** + * Gets the number of integers in the range [start, end) + */ + public get size(): number { + return this.end - this.start; + } + + /** + * Returns a range containing the single byte. + */ + public static single(byte: number): Range { + return new Range(byte, byte + 1); + } + + /** + * Creates a new range representing [start, end], inclusive. + */ + public static inclusive(start: number, end: number): Range { + return end >= start ? new Range(start, end + 1) : new Range(start + 1, end); + } + + /** + * @description Constructs a range object representing [start, end) + * @param start Represents the start of the range + * @param end Represents the end of the range + * @param direction The direction of the range, inferred from + * argument order if not provided. + */ + constructor( + public readonly start: number, + public readonly end: number = Number.MAX_SAFE_INTEGER, + direction?: RangeDirection + ) { + if (start < 0) { + throw new Error("Cannot construct a range with a negative start"); + } + + if (end < start) { + [this.start, this.end] = [end, start]; + direction ??= RangeDirection.Descending; + } else { + direction ??= RangeDirection.Ascending; + } + + this.direction = direction; + } + /** + * @desciption Tests if the given number if within the range + * @param {number} num The number to test + * @returns {boolean} True if the number is in the range, false otherwise + */ + public includes(num: number): boolean { + return num >= this.start && num < this.end; + } + + /** + * Expands the range to include the given value, if it is not already + * within the range. + */ + public expandToContain(value: number): Range { + if (value < this.start) { + return new Range(value, this.end, this.direction); + } else if (value >= this.end) { + return new Range(this.start, value + 1, this.direction); + } else { + return this; + } + } + /** + * Returns whether this range overlaps the other one. + */ + public overlaps(other: Range): boolean { + return other.end > this.start && other.start < this.end; + } + /** + * Returns one or more ranges representing ranges covered by exactly one of + * this or the `otherRange`. + */ + public difference(otherRange: Range): Range[] { + if (!this.overlaps(otherRange)) { + return [this, otherRange]; + } + + const delta: Range[] = []; + if (this.start !== otherRange.start) { + delta.push(new Range(otherRange.start, this.start)); + } + if (this.end !== otherRange.end) { + delta.push(new Range(otherRange.end, this.end)); + } + + return delta; + } +} + +/** + * Takes a DisplayContext-style list of ranges, where each range overlap + * toggles the selected state of the overlapped bytes, and returns the + * positively-selected data. + */ +export function getRangeSelectionsFromStack(ranges: readonly Range[]) { + const result: Range[] = []; + const pending = new Set(ranges); + const within = new Set(); + let last = -1; + while (pending.size || within.size) { + let nextStart: Range | undefined; + for (const range of pending) { + if (!nextStart || nextStart.start > range.start) { + nextStart = range; + } + } + + let nextEnd: Range | undefined; + for (const range of within) { + if (!nextEnd || nextEnd.end > range.end) { + nextEnd = range; + } + } + + if (nextStart && (!nextEnd || nextStart.start < nextEnd.end)) { + if (last !== -1 && within.size && within.size % 2 === 1 && last !== nextStart.start) { + result.push(new Range(last, nextStart.start)); + } + last = nextStart.start; + within.add(nextStart); + pending.delete(nextStart); + } else if (nextEnd) { + if (within.size % 2 === 1 && last !== nextEnd.end) { + result.push(new Range(last, nextEnd.end)); + } + last = nextEnd.end; + within.delete(nextEnd); + } + } + + return result; +} diff --git a/src/hexEditorProvider.ts b/src/hexEditorProvider.ts index 49516638..38b3d218 100644 --- a/src/hexEditorProvider.ts +++ b/src/hexEditorProvider.ts @@ -4,7 +4,7 @@ import TelemetryReporter from "@vscode/extension-telemetry"; import * as base64 from "js-base64"; import * as vscode from "vscode"; -import { HexDocumentEditReference } from "../shared/hexDocumentModel"; +import { HexDocumentEdit, HexDocumentEditOp, HexDocumentEditReference } from "../shared/hexDocumentModel"; import { Endianness, ExtensionHostMessageHandler, FromWebviewMessage, ICodeSettings, IEditorSettings, InspectorLocation, MessageHandler, MessageType, PasteMode, ToWebviewMessage } from "../shared/protocol"; import { deserializeEdits, serializeEdits } from "../shared/serialization"; import { DataInspectorView } from "./dataInspectorView"; @@ -259,6 +259,17 @@ export class HexEditorProvider implements vscode.CustomEditorProvider document.readBufferWithEdits(d.start, d.end - d.start))); + const edits = bytes.map((e, i): HexDocumentEdit => ({ + op: HexDocumentEditOp.Delete, + previous: e, + offset: message.deletes[i].start, + })); + messaging.sendEvent({ type: MessageType.SetEdits, edits: serializeEdits(edits), appendOnly: true }); + this.publishEdit(messaging, document, document.makeEdits(edits)); + return { type: MessageType.DeleteAccepted }; + } case MessageType.CancelSearch: document.searchProvider.cancel(); return; diff --git a/src/test/index.ts b/src/test/index.ts index 7aa304a6..7cc9ebe3 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -4,7 +4,7 @@ const fileImports = [ () => import("./backup.test"), () => import("./hexDocumentModel.test"), () => import("./searchRequest.test"), - () => import("./literalSearch.test"), + () => import("./range.test"), ]; export async function run(): Promise { diff --git a/src/test/range.test.ts b/src/test/range.test.ts new file mode 100644 index 00000000..505dfbc8 --- /dev/null +++ b/src/test/range.test.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +import { expect } from "chai"; +import { getRangeSelectionsFromStack, Range } from "../../shared/util/range"; + +describe("Range", () => { + + describe.only("getRangeSelectionsFromStack", () => { + it("works for a single", () => { + expect(getRangeSelectionsFromStack([new Range(10, 20)])).to.deep.equal([new Range(10, 20)]); + }); + + it("works for non-overlapping", () => { + expect(getRangeSelectionsFromStack([ + new Range(10, 20), + new Range(30, 40), + ])).to.deep.equal([ + new Range(10, 20), + new Range(30, 40), + ]); + }); + + it("excludes overlapping", () => { + expect(getRangeSelectionsFromStack([ + new Range(10, 20), + new Range(30, 40), + new Range(15, 35), + ])).to.deep.equal([ + new Range(10, 15), + new Range(20, 30), + new Range(35, 40), + ]); + }); + + it("excludes identity", () => { + expect(getRangeSelectionsFromStack([ + new Range(10, 20), + new Range(10, 20), + ])).to.deep.equal([]); + }); + + it("includes identity", () => { + expect(getRangeSelectionsFromStack([ + new Range(10, 20), + new Range(10, 20), + new Range(10, 20), + ])).to.deep.equal([new Range(10, 20)]); + }); + + it("pyramid#1", () => { + expect(getRangeSelectionsFromStack([ + new Range(0, 100), + new Range(10, 90), + new Range(20, 80), + new Range(30, 70), + new Range(40, 60), + ])).to.deep.equal([ + new Range(0, 10), + new Range(20, 30), + new Range(40, 60), + new Range(70, 80), + new Range(90, 100), + ]); + }); + + it("pyramid#2", () => { + expect(getRangeSelectionsFromStack([ + new Range(0, 50), + new Range(10, 60), + new Range(20, 70), + new Range(30, 80), + ])).to.deep.equal([ + new Range(0, 10), + new Range(20, 30), + new Range(50, 60), + new Range(70, 80), + ]); + }); + }); +});