diff --git a/src/dataEditor/dataEditorClient.ts b/src/dataEditor/dataEditorClient.ts index 75bdedeb0..03f9eb882 100644 --- a/src/dataEditor/dataEditorClient.ts +++ b/src/dataEditor/dataEditorClient.ts @@ -459,11 +459,17 @@ export class DataEditorClient implements vscode.Disposable { case MessageCommand.undoChange: await undo(this.omegaSessionId) await this.sendChangesInfo() + this.panel.webview.postMessage({ + command: MessageCommand.clearChanges, + }) break case MessageCommand.redoChange: await redo(this.omegaSessionId) await this.sendChangesInfo() + this.panel.webview.postMessage({ + command: MessageCommand.clearChanges, + }) break case MessageCommand.profile: @@ -525,6 +531,9 @@ export class DataEditorClient implements vscode.Disposable { ) { await clear(this.omegaSessionId) await this.sendChangesInfo() + this.panel.webview.postMessage({ + command: MessageCommand.clearChanges, + }) } break diff --git a/src/svelte/src/components/DataDisplays/CustomByteDisplay/DataLineFeed.svelte b/src/svelte/src/components/DataDisplays/CustomByteDisplay/DataLineFeed.svelte index 5dd49f187..f0e759928 100644 --- a/src/svelte/src/components/DataDisplays/CustomByteDisplay/DataLineFeed.svelte +++ b/src/svelte/src/components/DataDisplays/CustomByteDisplay/DataLineFeed.svelte @@ -31,6 +31,8 @@ limitations under the License. seekOffsetInput, visableViewports, dataDislayLineAmount, + replaceQuery, + searchResultsUpdated, } from '../../../stores' import { EditByteModes, @@ -62,10 +64,8 @@ limitations under the License. type CSSThemeClass, } from '../../../utilities/colorScheme' import { - selectionHighlights, - searchResultsHighlights, - updateSearchResultsHighlights, - searchResultsUpdated, + viewportByteIndicators, + categoryCSSSelectors, } from '../../../utilities/highlights' import { bytesPerRow } from '../../../stores' export let awaitViewportSeek: boolean @@ -169,7 +169,7 @@ limitations under the License. bytes: Array highlight: 'even' | 'odd' } - + enum ViewportScrollDirection { DECREMENT = -1, NONE = 0, @@ -181,10 +181,13 @@ limitations under the License. let viewportDataContainer: HTMLDivElement let selectedByteElement: HTMLDivElement let themeClass: CSSThemeClass - let activeSelection: Uint8Array let lineTopFileOffset: number - let searchResults: Uint8Array + let makingSelection = false + $: { + makingSelection = + $selectionDataStore.startOffset >= 0 && $selectionDataStore.active === false + } onMount(() => { viewportDataContainer = document.getElementById( CONTAINER_ID @@ -215,13 +218,10 @@ limitations under the License. } $: { - activeSelection = $selectionHighlights - searchResults = $searchResultsHighlights if ( - (viewportData.fileOffset >= 0 && - !awaitViewportSeek && - $dataFeedLineTop >= 0) || - $searchResultsUpdated + viewportData.fileOffset >= 0 && + !awaitViewportSeek && + $dataFeedLineTop >= 0 ) { if ( viewportLines.length !== 0 && @@ -243,6 +243,11 @@ limitations under the License. } } $: byteElementWidth = byteDivWidthFromRadix(dataRadix) +$: { + viewportByteIndicators.updateSelectionIndications($selectionDataStore) + viewportByteIndicators.updateSearchIndications($searchQuery, viewportData.fileOffset) + viewportByteIndicators.updateReplaceIndications($replaceQuery, viewportData.fileOffset) + } function generate_line_data( startIndex: number, @@ -359,21 +364,21 @@ limitations under the License. : atViewportHead && !atFileHead } - function mousedown(event: CustomEvent) { + function mousedown(event: ByteSelectionEvent) { selectionDataStore.update((selections) => { selections.active = false - selections.startOffset = event.detail.targetByte.offset + selections.startOffset = event.targetByte.offset selections.endOffset = -1 selections.originalEndOffset = -1 return selections }) } - function mouseup(event: CustomEvent) { + function mouseup(event: ByteSelectionEvent) { selectionDataStore.update((selections) => { selections.active = true - selections.endOffset = event.detail.targetByte.offset - selections.originalEndOffset = event.detail.targetByte.offset + selections.endOffset = event.targetByte.offset + selections.originalEndOffset = event.targetByte.offset adjust_event_offsets() return selections }) @@ -383,7 +388,7 @@ limitations under the License. return } - set_byte_selection(event.detail) + setByteSelection(event) } function adjust_event_offsets() { @@ -391,13 +396,15 @@ limitations under the License. const end = $selectionDataStore.endOffset if (start > end) { - $selectionDataStore.startOffset = end - $selectionDataStore.originalEndOffset = start - $selectionDataStore.endOffset = start + selectionDataStore.update( selections => { + selections.startOffset = end + selections.endOffset = start + return selections + }) } } - function set_byte_selection(selectionEvent: ByteSelectionEvent) { + function setByteSelection(selectionEvent: ByteSelectionEvent) { $focusedViewportId = selectionEvent.fromViewport $selectedByte = @@ -471,6 +478,47 @@ limitations under the License. } } + function mouseover_handler(e: Event) { + if(!makingSelection) return + + const target = e.target as HTMLDivElement + let targetViewportIndex = parseInt(target.getAttribute('offset')!) + + selectionDataStore.update((selections) => { + selections.endOffset = targetViewportIndex + adjust_event_offsets() + return selections + }) + } + + function mouseclick_handler(e: Event) { + const type = e.type + const targetElement = e.target as HTMLDivElement + let targetViewportIndex = parseInt(targetElement.getAttribute('offset')!) + let byteText: string | undefined = targetElement.innerHTML + let byteValue: number = byteText === undefined ? -1 : parseInt(byteText) + + let targetByte: ByteValue = { + offset: targetViewportIndex, + text: byteText, + value: byteValue + } + const byteSelectionEvent: ByteSelectionEvent = + { + targetElement: targetElement, + targetByte: targetByte, + fromViewport: targetElement.id.includes('logical') ? 'logical' : 'physical', + } + + switch(type) { + case 'mousedown': + mousedown(byteSelectionEvent) + break + case 'mouseup': + mouseup(byteSelectionEvent) + } + } + window.addEventListener('keydown', navigation_keydown_event) window.addEventListener('message', (msg) => { switch (msg.data.command) { @@ -485,30 +533,31 @@ limitations under the License. selectedByteElement = document.getElementById( $selectedByte.offset.toString() ) as HTMLDivElement - - updateSearchResultsHighlights( - $searchQuery.searchResults, - viewportData.fileOffset, - $searchQuery.byteLength - ) } break } }) + -{#if $selectionDataStore.active && $editMode === EditByteModes.Single} - {#key $selectedByte || selectedByteElement || dataRadix || $editorActionsAllowed === EditActionRestrictions.None} - - {/key} -{/if} -
+ + + +
+ {#if $selectionDataStore.active && $editMode == EditByteModes.Single} + {#key $selectedByte || selectedByteElement || dataRadix || $editorActionsAllowed == EditActionRestrictions.None} + + {/key} + {/if} {#each viewportLines as viewportLine, i}
@@ -523,17 +572,10 @@ limitations under the License. {#each viewportLine.bytes as byte} > - activeSelection[byte.offset]} id={'physical'} - radix={dataRadix} + categoryIndicationSelectors={categoryCSSSelectors($viewportByteIndicators[byte.offset])} width={byteElementWidth} disabled={byte.value === -1} - bind:selectionData={$selectionDataStore} - on:mouseup={mouseup} - on:mousedown={mousedown} /> {/each}
@@ -546,17 +588,10 @@ limitations under the License. {#each viewportLine.bytes as byte} > - activeSelection[byte.offset]} + categoryIndicationSelectors={categoryCSSSelectors($viewportByteIndicators[byte.offset])} id={'logical'} - radix={dataRadix} width={byteElementWidth} disabled={byte.value === -1} - bind:selectionData={$selectionDataStore} - on:mouseup={mouseup} - on:mousedown={mousedown} /> {/each}
@@ -697,15 +732,6 @@ limitations under the License. flex-direction: column; margin: 0 5px; } - span.submit-bpr-input { - font-size: 14px; - cursor: pointer; - margin: 0 5px; - } - span.submit-bpr-input:hover { - font-weight: bold; - cursor: pointer; - } div.container { display: flex; flex-direction: column; diff --git a/src/svelte/src/components/DataDisplays/CustomByteDisplay/DataValue.svelte b/src/svelte/src/components/DataDisplays/CustomByteDisplay/DataValue.svelte index ab12df431..e839ff17c 100644 --- a/src/svelte/src/components/DataDisplays/CustomByteDisplay/DataValue.svelte +++ b/src/svelte/src/components/DataDisplays/CustomByteDisplay/DataValue.svelte @@ -15,61 +15,18 @@ See the License for the specific language governing permissions and limitations under the License. --> @@ -78,33 +35,21 @@ limitations under the License. {:else if id === 'physical'}
{byte.text}
{:else}
{latin1Undefined(byte.value) ? '' : String.fromCharCode(byte.value)}
@@ -118,29 +63,24 @@ limitations under the License. align-items: center; flex-direction: row; font-family: var(--monospace-font); - /* border-radius: 5px; */ border-style: solid; border-width: 2px; border-color: transparent; height: 20px; text-align: center; transition: all 0.25s; - } - div.byte.isSelected, - div.byte.isSearchResult, - div.byte.possibleSelection { border-radius: 5px; + user-select: none; } - div.byte.isSelected { + div.byte.selected { background-color: var(--color-secondary-light); color: var(--color-secondary-darkest); } - div.byte.isSearchResult { - background-color: var(--color-tertiary-light); - color: var(--color-secondary-darkest); + div.byte.searchresult { + border-color: var(--color-search-result); } - div.byte.possibleSelection { - border-color: var(--color-secondary-light); + div.byte.replacement { + border-color: var(--color-replace-result); } div.byte:hover { border-color: var(--color-secondary-mid); diff --git a/src/svelte/src/components/DataDisplays/CustomByteDisplay/SelectedByteEdit.svelte b/src/svelte/src/components/DataDisplays/CustomByteDisplay/SelectedByteEdit.svelte index 251b0f7ee..691511047 100644 --- a/src/svelte/src/components/DataDisplays/CustomByteDisplay/SelectedByteEdit.svelte +++ b/src/svelte/src/components/DataDisplays/CustomByteDisplay/SelectedByteEdit.svelte @@ -343,7 +343,7 @@ limitations under the License. class="delete {themeClass}" id={actionElements['delete'].id} style:width={elementDivWidth} - on:click={send_delete} + on:mousedown|stopPropagation={send_delete} > ✖ @@ -358,7 +358,7 @@ limitations under the License. class="insert-before {themeClass}" id={actionElements['insert-before'].id} style:width={elementDivWidth} - on:click={send_insert} + on:mousedown|stopPropagation={send_insert} > ⇤ @@ -372,7 +372,7 @@ limitations under the License. class="insert-after {themeClass}" id={actionElements['insert-after'].id} style:width={elementDivWidth} - on:click={send_insert} + on:mousedown|stopPropagation={send_insert} > ⇥ diff --git a/src/svelte/src/components/DataDisplays/Header/DisplayHeader.svelte b/src/svelte/src/components/DataDisplays/Header/DisplayHeader.svelte index 4628e7858..1bf29003b 100644 --- a/src/svelte/src/components/DataDisplays/Header/DisplayHeader.svelte +++ b/src/svelte/src/components/DataDisplays/Header/DisplayHeader.svelte @@ -20,11 +20,8 @@ limitations under the License. displayRadix, seekOffset, seekOffsetInput, - selectionDataStore, seekOffsetSearchType, - selectionSize, bytesPerRow, - viewport, visableViewports } from '../../../stores' import { @@ -34,11 +31,9 @@ limitations under the License. RADIX_OPTIONS, } from '../../../stores/configuration' import { UIThemeCSSClass } from '../../../utilities/colorScheme' - import { createEventDispatcher } from 'svelte' import { OffsetSearchType } from '../../Header/fieldsets/SearchReplace' import { byteDivWidthFromRadix } from '../../../utilities/display' let bitIndexStr = '01234567' - let selectionOffsetText: string let offsetLine: string[] = [] $: { @@ -49,14 +44,6 @@ limitations under the License. ) } - $: selectionOffsetText = setSelectionOffsetInfo( - 'Selection', - $viewport.fileOffset + $selectionDataStore.startOffset, - $viewport.fileOffset + $selectionDataStore.endOffset, - $selectionSize, - $addressRadix - ) - function generate_offset_headers( addressRadix: RadixValues, displayRadix: RadixValues, @@ -127,14 +114,14 @@ limitations under the License. {#if $displayRadix === RADIX_OPTIONS.Binary} {#each offsetLine as offset}
-
{offset}
-
{bitIndexStr}
+
{offset}
+
{bitIndexStr}
{/each} {:else} {#each offsetLine as offset}
- {offset} + {offset}
{/each} {/if} diff --git a/src/svelte/src/components/Header/fieldsets/FileMetrics.svelte b/src/svelte/src/components/Header/fieldsets/FileMetrics.svelte index 83d6365b5..73e714f95 100644 --- a/src/svelte/src/components/Header/fieldsets/FileMetrics.svelte +++ b/src/svelte/src/components/Header/fieldsets/FileMetrics.svelte @@ -19,7 +19,7 @@ limitations under the License. import FlexContainer from '../../layouts/FlexContainer.svelte' import { MessageCommand } from '../../../utilities/message' import { vscode } from '../../../utilities/vscode' - import { saveable, fileMetrics } from '../../../stores' + import { saveable, fileMetrics, replaceQuery } from '../../../stores' import { createEventDispatcher } from 'svelte' import SidePanel from '../../layouts/SidePanel.svelte' import ByteFrequencyGraph from '../../DataMetrics/DataMetrics.svelte' diff --git a/src/svelte/src/components/Header/fieldsets/SearchReplace.svelte b/src/svelte/src/components/Header/fieldsets/SearchReplace.svelte index 12c8c90b4..16f24c738 100644 --- a/src/svelte/src/components/Header/fieldsets/SearchReplace.svelte +++ b/src/svelte/src/components/Header/fieldsets/SearchReplace.svelte @@ -42,13 +42,8 @@ limitations under the License. import { createEventDispatcher } from 'svelte' import { UIThemeCSSClass } from '../../../utilities/colorScheme' import ToggleableButton from '../../Inputs/Buttons/ToggleableButton.svelte' - import { - clearSearchResultsHighlights, - updateSearchResultsHighlights, - } from '../../../utilities/highlights' - import { viewport } from '../../../stores' import { EditActionRestrictions } from '../../../stores/configuration' - import { OffsetSearchType } from './SearchReplace' + import { OffsetSearchType, clear_queryable_results } from './SearchReplace' import Tooltip from '../../layouts/Tooltip.svelte' const eventDispatcher = createEventDispatcher() @@ -201,7 +196,8 @@ limitations under the License. searchStarted = false replaceStarted = false matchOffset = -1 - clearSearchResultsHighlights() + clear_queryable_results() + eventDispatcher('clearDataDisplays') } @@ -210,25 +206,24 @@ limitations under the License. // handle search results case MessageCommand.searchResults: if (msg.data.data.searchResults.length > 0) { - $searchQuery.searchResults = msg.data.data.searchResults - $searchQuery.byteLength = msg.data.data.searchDataBytesLength + searchQuery.updateSearchResults(msg.data.data) switch (direction) { case 'Home': - hasNext = msg.data.data.overflow + hasNext = $searchQuery.overflow hasPrev = false break case 'End': hasNext = false - hasPrev = msg.data.data.overflow + hasPrev = $searchQuery.overflow break case 'Forward': - hasNext = msg.data.data.overflow + hasNext = $searchQuery.overflow hasPrev = justReplaced ? preReplaceHasPrev : true justReplaced = false break case 'Backward': hasNext = true - hasPrev = msg.data.data.overflow + hasPrev = $searchQuery.overflow break } matchOffset = $searchQuery.searchResults[0] @@ -240,17 +235,12 @@ limitations under the License. showReplaceOptions = true showSearchOptions = false } - $searchQuery.overflow = msg.data.data.overflow } else { matchOffset = -1 $searchQuery.overflow = showSearchOptions = showReplaceOptions = false + searchQuery.clear() } searchStarted = replaceStarted = false - updateSearchResultsHighlights( - $searchQuery.searchResults, - $viewport.fileOffset, - $searchQuery.byteLength - ) $searchQuery.processing = false break @@ -259,8 +249,11 @@ limitations under the License. searchStarted = replaceStarted = false if (msg.data.data.replacementsCount > 0) { // subtract 1 from the next offset because search next will add 1 - clearSearchResultsHighlights() matchOffset = msg.data.data.nextOffset - 1 + replaceQuery.addResult({ + byteLength: msg.data.data.replaceDataBytesLength, + offset: msg.data.data.nextOffset - msg.data.data.replaceDataBytesLength + }) preReplaceHasPrev = hasPrev justReplaced = true searchNext() @@ -270,6 +263,11 @@ limitations under the License. } $replaceQuery.processing = false break + + case MessageCommand.clearChanges: + cancel() + break + } }) diff --git a/src/svelte/src/components/Header/fieldsets/SearchReplace.ts b/src/svelte/src/components/Header/fieldsets/SearchReplace.ts index e1f739b94..e7311c6b7 100644 --- a/src/svelte/src/components/Header/fieldsets/SearchReplace.ts +++ b/src/svelte/src/components/Header/fieldsets/SearchReplace.ts @@ -16,8 +16,8 @@ */ import { SimpleWritable } from '../../../stores/localStore' -import { addressRadix, seekOffsetInput } from '../../../stores' -import { get } from 'svelte/store' +import { replaceQuery, searchQuery } from '../../../stores' +import { VIEWPORT_CAPACITY_MAX } from '../../../stores/configuration' export enum OffsetSearchType { ABSOLUTE, @@ -29,16 +29,38 @@ export type RelativeSeekSign = '+' | '-' interface QueryableData { input: string processing: boolean - isValid: boolean + initiaited: boolean + iterableDataFromOffset(offset: number): IndexCriteria } -class SearchData implements QueryableData { + +export type IndexCriteria = { + start: number + end: number + data: any[] +} +export class SearchData implements QueryableData { input: string = '' processing: boolean = false - isValid: boolean = false + initiaited: boolean = false searchIndex: number = 0 searchResults: Array = [] overflow: boolean = false byteLength: number = 0 + iterableDataFromOffset(offset: number): IndexCriteria { + const start = this.searchResults.findIndex((x) => x >= offset) + const end = this.searchResults.findIndex( + (x) => x >= offset + VIEWPORT_CAPACITY_MAX + ) + let ret: IndexCriteria = { + start: start, + end: end, + data: this.searchResults.slice( + start, + end >= 0 ? end : this.searchResults.length + ), + } + return ret + } } export class SearchQuery extends SimpleWritable { protected init(): SearchData { @@ -47,39 +69,79 @@ export class SearchQuery extends SimpleWritable { public clear() { this.update((query) => { query.processing = false + query.initiaited = false + query.initiaited = false query.searchIndex = 0 query.searchResults = [] return query }) } - public updateSearchResults(offset?: number) { + public updateSearchResults(msgData: any) { this.update((query) => { - query.searchIndex = !offset - ? Math.abs( - (query.searchResults.length + query.searchIndex) % - query.searchResults.length - ) - : Math.abs( - (query.searchResults.length + offset) % query.searchResults.length - ) - - seekOffsetInput.update((_) => { - return query.searchResults[query.searchIndex].toString( - get(addressRadix) - ) - }) + query.initiaited = true + query.searchResults = msgData.searchResults + query.byteLength = msgData.searchDataBytesLength + query.overflow = msgData.overflow return query }) } } -class ReplaceData implements QueryableData { +/** +Object that defines describes an instance of a replacement that occured during a Search & Replace query. +@param offset **File** offset of where the replacement occured. +@param byteLength Byte length of the replacement data. +*/ +export type DataReplacement = { + offset: number + byteLength: number +} + +export class ReplaceData implements QueryableData { input: string = '' processing: boolean = false - isValid: boolean = false + initiaited: boolean = false + results: Array = [] + iterableDataFromOffset(offset: number): IndexCriteria { + const start = this.results.findIndex((x) => x.offset >= offset) + const end = this.results.findIndex( + (x) => x.offset >= offset + VIEWPORT_CAPACITY_MAX + ) + let ret: IndexCriteria = { + start: start, + end: end, + data: this.results.slice(start, end >= 0 ? end : this.results.length), + } + return ret + } } export class ReplaceQuery extends SimpleWritable { protected init(): ReplaceData { return new ReplaceData() } + public addResult(result: DataReplacement) { + this.update((data) => { + data.initiaited = true + data.results.push(result) + return data + }) + } + public pop() { + this.update((data) => { + data.results.pop() + return data + }) + } + public clear() { + this.update((data) => { + data.results = [] + data.initiaited = false + return data + }) + } +} + +export function clear_queryable_results() { + searchQuery.clear() + replaceQuery.clear() } diff --git a/src/svelte/src/components/dataEditor.svelte b/src/svelte/src/components/dataEditor.svelte index 32b52854b..4b4ee429f 100644 --- a/src/svelte/src/components/dataEditor.svelte +++ b/src/svelte/src/components/dataEditor.svelte @@ -61,8 +61,8 @@ limitations under the License. ViewportData_t, } from './DataDisplays/CustomByteDisplay/BinaryData' import { byte_count_divisible_offset } from '../utilities/display' - import { clearSearchResultsHighlights } from '../utilities/highlights' import Help from './layouts/Help.svelte' + import { viewportByteIndicators } from '../utilities/highlights' $: $UIThemeCSSClass = $darkUITheme ? CSSThemeClass.Dark : CSSThemeClass.Light @@ -248,16 +248,15 @@ limitations under the License. function clearQueryableData() { searchQuery.clear() - clearSearchResultsHighlights() } function handleKeyBind(event: Event) { const kbdEvent = event as KeyboardEvent if (key_is_mappable(kbdEvent.key)) { - elementKeypressEventMap.run(document.activeElement.id, kbdEvent) + if(document.activeElement) // document.activeElement is possibly undefined / null + elementKeypressEventMap.run(document.activeElement.id, kbdEvent) return } - if ($editMode === EditByteModes.Multiple) return switch (kbdEvent.key) { case 'Escape': clearDataDisplays() diff --git a/src/svelte/src/components/globalStyles.css b/src/svelte/src/components/globalStyles.css index 916f9b3e3..765897f77 100644 --- a/src/svelte/src/components/globalStyles.css +++ b/src/svelte/src/components/globalStyles.css @@ -60,6 +60,8 @@ html { --color-tertiary-dark: #5f816b; --color-tertiary-darkest: #232f27; --color-alternate-grey: #bbb5bd; + --color-search-result: #69a0a7; + --color-replace-result: #5f816b; } /* Global LEGEND Styles */ diff --git a/src/svelte/src/components/layouts/Tooltip.svelte b/src/svelte/src/components/layouts/Tooltip.svelte index f04abff3d..e313e3415 100644 --- a/src/svelte/src/components/layouts/Tooltip.svelte +++ b/src/svelte/src/components/layouts/Tooltip.svelte @@ -101,7 +101,7 @@ limitations under the License. } .fit-content { - max-width: 150px; + max-width: 250px; min-width: 50px; max-height: 50px; min-height: 25px; diff --git a/src/svelte/src/stores/index.ts b/src/svelte/src/stores/index.ts index 66b3c8ea0..fe60eb1a3 100644 --- a/src/svelte/src/stores/index.ts +++ b/src/svelte/src/stores/index.ts @@ -43,7 +43,6 @@ import { type RadixValues, type BytesPerRow, EditActionRestrictions, - VIEWPORT_CAPACITY_MAX, } from './configuration' import type { AvailableHelpSections } from '../components/layouts/Help' @@ -60,6 +59,34 @@ export class SelectionData_t { this.originalEndOffset >= 0 ) } + public editedLength(): number { + return this.endOffset - this.startOffset + } + public originalLength(): number { + return this.originalEndOffset - this.startOffset + } + public selectionIndexIsAnEdit(index: number): boolean { + const editOffsetLength = this.originalLength() - this.editedLength() + const editStartOffset = this.originalEndOffset - editOffsetLength + + return index <= this.originalEndOffset + ? index >= editStartOffset + : index <= editStartOffset + } + public isEmpty(): boolean { + return this.editedLength() + 1 == 0 + } + public editStartOffset() { + return ( + this.originalEndOffset - (this.originalLength() - this.editedLength()) + ) + } + public editLengthDelta() { + return this.endOffset - this.originalEndOffset + 1 + } + public makingSelection() { + return this.startOffset >= 0 && this.originalEndOffset === -1 + } } class SelectionData extends SimpleWritable { @@ -76,6 +103,10 @@ export enum EditModeRestrictions { OverwriteOnly, } +/**************************************************************************/ +/* Writable Stores */ +/**************************************************************************/ + // noinspection JSUnusedGlobalSymbols // theme to use for the UI @@ -155,6 +186,12 @@ export const dataDislayLineAmount = writable(20) export type VisibleViewports = 'physical' | 'logical' | 'all' export const visableViewports = writable('all' as VisibleViewports) + +export const searchResultsUpdated = writable(false) + +/**************************************************************************/ +/* Derived Stores */ +/**************************************************************************/ // Can the user's selection derive both edit modes? export const regularSizedFile = derived(fileMetrics, ($fileMetrics) => { return $fileMetrics.computedSize >= 2 @@ -204,7 +241,7 @@ export const replaceable = derived( } if ($selectionData.active) { replaceErr.update(() => { - return 'Cannot replace while viewport data is selected' + return "Can't replace while selection active" }) return false } diff --git a/src/svelte/src/utilities/ByteCategories/CategoryIndications.ts b/src/svelte/src/utilities/ByteCategories/CategoryIndications.ts new file mode 100644 index 000000000..a7d25a02c --- /dev/null +++ b/src/svelte/src/utilities/ByteCategories/CategoryIndications.ts @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + NoIndication, + type IByteIndication, + ByteIndication, +} from './IIndication' + +export class ByteCategory { + private _indicators: IByteIndication[] = [NoIndication] + + constructor( + private _name: string, + private _bitLength: number + ) { + if (_bitLength > 8) + throw new Error('Byte category indications cannot exceed 8 bits.') + } + addIndication(selectorName: string) { + this._indicators.push(new ByteIndication(selectorName)) + return this + } + bitLength() { + return this._bitLength + } + name() { + return this._name + } + categories() { + return this._indicators + } + indicationByIndex(index: number) { + return index >= this._indicators.length || index < 0 + ? this._indicators[0] + : this._indicators[index] + } + indicationByName(category: string) { + const target = category + let ret = -1 + + this._indicators.forEach((categoryObj, i) => { + if (categoryObj.selector() === target) ret = i + }) + if (ret < 0) throw new Error(`Indication category "${category}" not found.`) + else return ret + } +} + +export const CategoryOne = new ByteCategory('one', 4) +CategoryOne.addIndication('selected') + +export const CategoryTwo = new ByteCategory('two', 4) +CategoryTwo.addIndication('searchresult').addIndication('replacement') + +class ByteIndicationCategories { + private _categories: { + [key: string]: ByteCategory + } = {} + private _bitsUtilized: number = 0 + + public addIndicationCategory(category: ByteCategory) { + if (this._bitsUtilized + category.bitLength() > 8) + throw new Error('Category addition would exceed bit limit') + this._categories[category.name()] = category + this._bitsUtilized += category.bitLength() + + return this + } + public category(name: string): ByteCategory { + return this._categories[name] + } + public categoryValueByIndication( + category: ByteCategory, + indicationName: string + ): number { + return ( + category.indicationByName(indicationName) << this.categoryBitPos(category) + ) + } + public clearCategoryFrom(data: Uint8Array, category: ByteCategory) { + data.forEach((byte, i, data) => { + data[i] &= this.categoryMask(category) ^ 0xff + }) + } + public categoryCSSSelector( + category: ByteCategory, + byteIndicationValue: number + ) { + const maskedByteValue = byteIndicationValue & this.categoryMask(category) + const indicationIndex = maskedByteValue >> this.categoryBitPos(category) + const indication = category.categories()[indicationIndex] + return indication != undefined + ? indication.selector() + : category.indicationByName('none') + } + private categoryMask(category: ByteCategory): number { + const categoryBitPos = this.categoryBitPos(category) + const categoryBitLength = category.bitLength() + return (Math.pow(2, categoryBitLength) - 1) << categoryBitPos + } + private indexOf(category: ByteCategory) { + let i = 0 + for (let _category in this._categories) { + if (category.name() === this._categories[_category].name()) { + return i + } + i++ + } + return -1 + } + private categoryBitPos(category: ByteCategory): number { + return this.indexOf(category) * category.bitLength() + } +} + +export const ViewportByteCategories = new ByteIndicationCategories() +ViewportByteCategories.addIndicationCategory(CategoryOne).addIndicationCategory( + CategoryTwo +) diff --git a/src/svelte/src/utilities/ByteCategories/IIndication.ts b/src/svelte/src/utilities/ByteCategories/IIndication.ts new file mode 100644 index 000000000..7a6dede5e --- /dev/null +++ b/src/svelte/src/utilities/ByteCategories/IIndication.ts @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface IByteIndication { + selector(): string + equals(categoryArg: IByteIndication): boolean +} + +class NullIndication implements IByteIndication { + equals(categoryArg: IByteIndication): boolean { + return this.selector() === categoryArg.selector() + } + selector(): string { + return 'none' + } +} + +export class ByteIndication implements IByteIndication { + constructor(private _selector: string) {} + equals(categoryArg: IByteIndication): boolean { + return this.selector() === categoryArg.selector() + } + selector(): string { + return this._selector.toLowerCase() + } +} + +export const NoIndication: NullIndication = new NullIndication() diff --git a/src/svelte/src/utilities/display.ts b/src/svelte/src/utilities/display.ts index 6b0f522a3..481d5390b 100644 --- a/src/svelte/src/utilities/display.ts +++ b/src/svelte/src/utilities/display.ts @@ -12,7 +12,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - import { EditByteModes, type BytesPerRow, diff --git a/src/svelte/src/utilities/highlights.ts b/src/svelte/src/utilities/highlights.ts index bb00644db..7e5146d89 100644 --- a/src/svelte/src/utilities/highlights.ts +++ b/src/svelte/src/utilities/highlights.ts @@ -15,62 +15,148 @@ * limitations under the License. */ -import { derived, readable, writable } from 'svelte/store' -import { selectionDataStore } from '../stores' +import { SelectionData_t, searchResultsUpdated } from '../stores' +import { VIEWPORT_CAPACITY_MAX } from '../stores/configuration' +import { SimpleWritable } from '../stores/localStore' +import type { + DataReplacement, + ReplaceData, + SearchData, +} from '../components/Header/fieldsets/SearchReplace' +import { ViewportByteCategories } from './ByteCategories/CategoryIndications' -let selectionHighlightLUT = new Uint8Array(1024) -export let selectionHighlightMask = writable(0) - -let searchResultsHighlightLUT = new Uint8Array(1024).fill(0) +class ViewportByteIndications extends SimpleWritable { + protected init(): Uint8Array { + return new Uint8Array(VIEWPORT_CAPACITY_MAX).fill(0) + } + public updateSearchIndications( + searchQuery: SearchData, + viewportFileOffset: number + ) { + const searchCategory = ViewportByteCategories.category('two') + if (searchQuery.searchResults.length === 0) { + this.store.update((indications) => { + ViewportByteCategories.clearCategoryFrom(indications, searchCategory) + return indications + }) + } else { + this.store.update((indications) => { + const start = searchQuery.searchResults[0] - viewportFileOffset + const end = start + searchQuery.byteLength + for (let i = 0; i < VIEWPORT_CAPACITY_MAX; i++) { + if (i >= start && i < end) + indications[i] |= ViewportByteCategories.categoryValueByIndication( + searchCategory, + 'searchresult' + ) + else + indications[i] &= ViewportByteCategories.categoryValueByIndication( + searchCategory, + 'none' + ) + } + searchResultsUpdated.set(true) + return indications + }) + } + } -export enum HightlightCategoryMasks { - None = 0, - ActiveSelection = 1, - ConsideredForSelection = 2, - SearchResult = 4, -} + public updateReplaceIndications(replaceData: ReplaceData, vpOffset: number) { + const replaceCategory = ViewportByteCategories.category('two') -export const selectionHighlights = derived( - [selectionDataStore, selectionHighlightMask], - ([$selectionData, $selectionHighlightMask]) => { - let start = $selectionData.startOffset - let end = - $selectionHighlightMask === 0 - ? $selectionData.originalEndOffset - : $selectionData.endOffset - if (start > end && end > -1) [start, end] = [end, start] + if (replaceData.results.length === 0 && replaceData.initiaited) { + this.store.update((indications) => { + ViewportByteCategories.clearCategoryFrom(indications, replaceCategory) + return indications + }) + } else { + this.store.update((indications) => { + const viewportFileOffset = vpOffset + const matchingReplacements = + replaceData.iterableDataFromOffset(viewportFileOffset) + matchingReplacements.data.forEach((resultIndex) => { + const { offset, byteLength } = resultIndex as DataReplacement + for (let i = 0; i < byteLength; i++) + indications[offset - viewportFileOffset + i] |= + ViewportByteCategories.categoryValueByIndication( + replaceCategory, + 'replacement' + ) + }) + return indications + }) + } + } + public updateSelectionIndications(selectionData: SelectionData_t) { + const category1 = ViewportByteCategories.category('one') + const start = selectionData.startOffset + const editedEnd = selectionData.endOffset + 1 + const originalEnd = selectionData.originalEndOffset - for (let i = 0; i < 1024; i++) { - selectionHighlightLUT[i] = - i >= start && i <= end ? 1 << $selectionHighlightMask : 0 + if (!selectionData.makingSelection() && !selectionData.active) { + this.store.update((indications) => { + ViewportByteCategories.clearCategoryFrom(indications, category1) + return indications + }) + } + if (selectionData.active || selectionData.makingSelection()) { + const offsetPartitions = [ + generateSelectionCategoryParition(0, start, (byte) => { + byte[0] = category1.indicationByName('none') + }), + generateSelectionCategoryParition(start, editedEnd, (byte) => { + byte[0] = category1.indicationByName('selected') + }), + generateSelectionCategoryParition( + Math.max(originalEnd, editedEnd), + VIEWPORT_CAPACITY_MAX - start, + (byte) => { + byte[0] = category1.indicationByName('none') + } + ), + ] + this.store.update((indications) => { + for (const partition of offsetPartitions) { + for (let i = partition.start; i < partition.end; i++) + partition.assignByte(indications.subarray(i, i + 1)) + } + return indications + }) } + } +} - return selectionHighlightLUT +export const viewportByteIndicators = new ViewportByteIndications() + +type CategoryOffsetParition = { + start: number + end: number + assignByte: (byte: Uint8Array) => void +} +function generateSelectionCategoryParition( + start: number, + end: number, + assignmentFn: (byte: Uint8Array) => void +): CategoryOffsetParition { + return { + start, + end, + assignByte: assignmentFn, } -) +} -export const searchResultsHighlights = readable(searchResultsHighlightLUT) -export const searchResultsUpdated = writable(false) -export function updateSearchResultsHighlights( - data: number[], - viewportFileOffset: number, - byteWidth: number -) { - const criteriaStart = data.findIndex((x) => x >= viewportFileOffset) - const criteriaEnd = data.findIndex((x) => x >= viewportFileOffset + 1024) - const searchCriteria = data.slice( - criteriaStart, - criteriaEnd >= 0 ? criteriaEnd : data.length +export function categoryCSSSelectors(byteIndicationValue: number): string { + let ret = '' + const CategoryOneSelector = ViewportByteCategories.categoryCSSSelector( + ViewportByteCategories.category('one'), + byteIndicationValue + ) + const CategoryTwoSelector = ViewportByteCategories.categoryCSSSelector( + ViewportByteCategories.category('two'), + byteIndicationValue ) - searchResultsHighlightLUT.fill(0) + ret += CategoryOneSelector + ' ' + CategoryTwoSelector - searchCriteria.forEach((offset) => { - for (let i = 0; i < byteWidth; i++) - searchResultsHighlightLUT[offset - viewportFileOffset + i] = 1 - }) - searchResultsUpdated.set(true) -} -export function clearSearchResultsHighlights() { - searchResultsHighlightLUT.fill(0) + return ret }