Skip to content

Commit

Permalink
Implemented Indexable ByteValue Indications
Browse files Browse the repository at this point in the history
- Added indexable indication array for ByteValues within the viewport displays.
- Created categorical byte indiciations values & CSS selectors.

Closes #784
scholarsmate authored and stricklandrbls committed Dec 21, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent f31364c commit d7f7088
Showing 17 changed files with 638 additions and 252 deletions.
9 changes: 9 additions & 0 deletions src/dataEditor/dataEditorClient.ts
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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<ByteValue>
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,10 @@ 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 +363,21 @@ limitations under the License.
: atViewportHead && !atFileHead
}
function mousedown(event: CustomEvent<ByteSelectionEvent>) {
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<ByteSelectionEvent>) {
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,21 +387,23 @@ limitations under the License.
return
}
set_byte_selection(event.detail)
setByteSelection(event)
}
function adjust_event_offsets() {
const start = $selectionDataStore.startOffset
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 +477,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 +532,31 @@ limitations under the License.
selectedByteElement = document.getElementById(
$selectedByte.offset.toString()
) as HTMLDivElement
updateSearchResultsHighlights(
$searchQuery.searchResults,
viewportData.fileOffset,
$searchQuery.byteLength
)
}
break
}
})
</script>

{#if $selectionDataStore.active && $editMode === EditByteModes.Single}
{#key $selectedByte || selectedByteElement || dataRadix || $editorActionsAllowed === EditActionRestrictions.None}
<SelectedByteEdit
byte={$selectedByte}
on:seek
on:applyChanges
on:handleEditorEvent
/>
{/key}
{/if}

<div class="container" style:height id={CONTAINER_ID}>
<svelte:window on:mousemove={mouseover_handler}/>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="container" style:height id={CONTAINER_ID}
on:mousedown={mouseclick_handler}
on:mouseup={mouseclick_handler}
>
{#if $selectionDataStore.active && $editMode == EditByteModes.Single}
{#key $selectedByte || selectedByteElement || dataRadix || $editorActionsAllowed == EditActionRestrictions.None}
<SelectedByteEdit
byte={$selectedByte}
on:seek
on:applyChanges
on:handleEditorEvent
/>
{/key}
{/if}
{#each viewportLines as viewportLine, i}
<div class={`line ${viewportLine.highlight} ${themeClass}`}>
<div class="address" id="address">
@@ -523,17 +571,10 @@ limitations under the License.
{#each viewportLine.bytes as byte}
<DataValue
{byte}
isSelected={activeSelection[byte.offset] === 1}
possibleSelection={activeSelection[byte.offset] === 2}
isSearchResult={searchResults[byte.offset] >>
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}
</div>
@@ -546,17 +587,10 @@ limitations under the License.
{#each viewportLine.bytes as byte}
<DataValue
{byte}
isSelected={activeSelection[byte.offset] === 1}
possibleSelection={activeSelection[byte.offset] === 2}
isSearchResult={searchResults[byte.offset] >>
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}
</div>
@@ -697,15 +731,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;
Original file line number Diff line number Diff line change
@@ -15,61 +15,18 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import {
latin1Undefined,
type ByteSelectionEvent,
type ByteValue,
type ViewportDataType,
} from './BinaryData'
import type { SelectionData_t } from '../../../stores'
import type { ByteDivWidth } from '../../../utilities/display'
import type { RadixValues } from '../../../stores/configuration'
import { selectionHighlightMask } from '../../../utilities/highlights'
export let id: ViewportDataType
export let byte: ByteValue
export let selectionData: SelectionData_t
export let radix: RadixValues
export let disabled = false
export let width: ByteDivWidth = '20px'
export let isSelected = false
export let possibleSelection = false
export let isSearchResult = 0
const eventDispatcher = createEventDispatcher()
let makingSelection = false
$: {
makingSelection =
selectionData.startOffset >= 0 && selectionData.active === false
$selectionHighlightMask = makingSelection === true ? 1 : 0
}
function mouse_enter_handle(event: MouseEvent) {
if (!makingSelection) return
if (disabled) {
selectionData.endOffset = -1
makingSelection = false
return
}
selectionData.endOffset = byte.offset
}
function mouse_leave_handle(event: MouseEvent) {
if (!makingSelection) return
selectionData.endOffset = -1
}
function mouse_event_handle(event: MouseEvent) {
const type = event.type
const targetElement = event.target
if (id === 'logical') byte.text = String.fromCharCode(byte.value)
eventDispatcher(type, {
targetElement: targetElement,
targetByte: byte,
fromViewport: id,
} as ByteSelectionEvent)
}
export let categoryIndicationSelectors: string
</script>

<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -78,33 +35,21 @@ limitations under the License.
{:else if id === 'physical'}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="byte"
class:isSelected
class:isSearchResult
class:possibleSelection
class={'byte ' + categoryIndicationSelectors}
id={id + '-' + byte.offset.toString()}
style:width
on:mouseup={mouse_event_handle}
on:mousedown={mouse_event_handle}
on:mouseenter={mouse_enter_handle}
on:mouseleave={mouse_leave_handle}
offset={byte.offset}
>
{byte.text}
</div>
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="byte"
class:isSelected
class:isSearchResult
class:possibleSelection
class={'byte ' + categoryIndicationSelectors}
id={id + '-' + byte.offset.toString()}
style:width={'20px'}
class:latin1Undefined={latin1Undefined(byte.value)}
on:mouseup={mouse_event_handle}
on:mousedown={mouse_event_handle}
on:mouseenter={mouse_enter_handle}
on:mouseleave={mouse_leave_handle}
offset={byte.offset}
>
{latin1Undefined(byte.value) ? '' : String.fromCharCode(byte.value)}
</div>
@@ -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);
Original file line number Diff line number Diff line change
@@ -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}
>
<Tooltip alwaysEnabled={true} description={'Delete byte'}>
&#10006;
@@ -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}
>
<Tooltip alwaysEnabled={true} description={'Insert as preceding byte'}>
&#8676;
@@ -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}
>
<Tooltip alwaysEnabled={true} description={'Insert as following byte'}>
&#8677;
19 changes: 3 additions & 16 deletions src/svelte/src/components/DataDisplays/Header/DisplayHeader.svelte
Original file line number Diff line number Diff line change
@@ -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}
<div class="physical-addr-seg binary" style:min-width={byteDivWidthFromRadix($displayRadix)}>
<div>{offset}</div>
<div>{bitIndexStr}</div>
<div><b>{offset}</b></div>
<div><b>{bitIndexStr}</b></div>
</div>
{/each}
{:else}
{#each offsetLine as offset}
<div class="physical-addr-seg" style:min-width={byteDivWidthFromRadix($displayRadix)}>
{offset}
<b>{offset}</b>
</div>
{/each}
{/if}
Original file line number Diff line number Diff line change
@@ -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'
38 changes: 18 additions & 20 deletions src/svelte/src/components/Header/fieldsets/SearchReplace.svelte
Original file line number Diff line number Diff line change
@@ -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
}
})
</script>
112 changes: 90 additions & 22 deletions src/svelte/src/components/Header/fieldsets/SearchReplace.ts
Original file line number Diff line number Diff line change
@@ -16,8 +16,9 @@
*/

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'
import { viewportByteIndicators } from '../../../utilities/highlights'

export enum OffsetSearchType {
ABSOLUTE,
@@ -29,16 +30,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<number> = []
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<SearchData> {
protected init(): SearchData {
@@ -47,39 +70,84 @@ export class SearchQuery extends SimpleWritable<SearchData> {
public clear() {
this.update((query) => {
query.processing = false
query.initiaited = false
query.searchIndex = 0
query.searchResults = []
viewportByteIndicators.clearIndication('searchresult')
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<DataReplacement> = []
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
)
const withinRange = start >= 0
const data = withinRange
? this.results.slice(start, end >= 0 ? end : this.results.length)
: []
let ret: IndexCriteria = {
start: start,
end: end,
data: data,
}
return ret
}
}
export class ReplaceQuery extends SimpleWritable<ReplaceData> {
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.processing = false
data.results = []
data.initiaited = false
return data
})
}
}

export function clear_queryable_results() {
searchQuery.clear()
replaceQuery.clear()
}
7 changes: 3 additions & 4 deletions src/svelte/src/components/dataEditor.svelte
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 2 additions & 0 deletions src/svelte/src/components/globalStyles.css
Original file line number Diff line number Diff line change
@@ -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 */
2 changes: 1 addition & 1 deletion src/svelte/src/components/layouts/Tooltip.svelte
Original file line number Diff line number Diff line change
@@ -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;
41 changes: 39 additions & 2 deletions src/svelte/src/stores/index.ts
Original file line number Diff line number Diff line change
@@ -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<SelectionData_t> {
@@ -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
}
114 changes: 114 additions & 0 deletions src/svelte/src/utilities/ByteCategories/CategoryIndications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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 { CategoryOne, type ByteCategory, CategoryTwo } from './IByteCategory'

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 categoryOfIndication(indicationName: string): ByteCategory {
for (const category in this._categories)
if (this._categories[category].contains(indicationName))
return this._categories[category]

throw new Error(
`No ByteCategory found with ByteIndication named ${indicationName}`
)
}
public categoryValueByIndication(
category: ByteCategory,
indicationName: string
): number {
return category.indexOf(indicationName) << this.categoryBitPos(category)
}
public clearIndication(data: Uint8Array, indicationName: string) {
const category = this.categoryOfIndication(indicationName)
data.forEach((byte, i, data) => {
const categoryValueOfByte = byte & this.categoryMask(category)
if (
categoryValueOfByte ===
this.categoryValueByIndication(category, indicationName)
)
data[i] &= this.categoryMask(category) ^ 0xff
})
}
public clearAndSetIf(
data: Uint8Array,
indication: string,
predicate: (byte: number, index: number) => boolean
) {
const category = this.categoryOfIndication(indication)
if (!category) {
console.error(`No ByteCategory contains the indication: ${indication}`)
return
}

data.forEach((byte, i, data) => {
data[i] &= this.categoryMask(category) ^ 0xff
if (predicate(byte, i))
data[i] |= this.categoryValueByIndication(category, indication)
})
}
public categoryCSSSelector(
category: ByteCategory,
byteIndicationValue: number
) {
const maskedByteValue = byteIndicationValue & this.categoryMask(category)
const indicationIndex = maskedByteValue >> this.categoryBitPos(category)
const indication = category.indicators()[indicationIndex]
return indication != undefined
? indication.selector()
: category.indexOf('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
)
84 changes: 84 additions & 0 deletions src/svelte/src/utilities/ByteCategories/IByteCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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 interface IByteCategory {
addIndication(selectorName: string): void
bitLength(): number
name(): string
indicators(): readonly IByteIndication[]
at(index: number): IByteIndication
indexOf(selectorName: string): number
contains(selectorName: string): boolean
}

export class ByteCategory implements IByteCategory {
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
}
indicators() {
return this._indicators
}
at(index: number) {
return index >= this._indicators.length || index < 0
? this._indicators[0]
: this._indicators[index]
}
indexOf(selectorName: string) {
const target = selectorName
let ret = -1

this._indicators.forEach((categoryObj, i) => {
if (categoryObj.selector() === target) ret = i
})
if (ret < 0)
throw new Error(`Indication category "${selectorName}" not found.`)
else return ret
}
contains(selectorName: string): boolean {
for (let i = 0; i < this._indicators.length; i++)
if (this._indicators[i].selector() == selectorName) return true
return false
}
}

export const CategoryOne = new ByteCategory('one', 4)
CategoryOne.addIndication('selected')

export const CategoryTwo = new ByteCategory('two', 4)
CategoryTwo.addIndication('searchresult').addIndication('replacement')
42 changes: 42 additions & 0 deletions src/svelte/src/utilities/ByteCategories/IIndication.ts
Original file line number Diff line number Diff line change
@@ -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()
1 change: 0 additions & 1 deletion src/svelte/src/utilities/display.ts
Original file line number Diff line number Diff line change
@@ -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,
174 changes: 128 additions & 46 deletions src/svelte/src/utilities/highlights.ts
Original file line number Diff line number Diff line change
@@ -15,62 +15,144 @@
* 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)
class ViewportByteIndications extends SimpleWritable<Uint8Array> {
protected init(): Uint8Array {
return new Uint8Array(VIEWPORT_CAPACITY_MAX).fill(0)
}
public clearIndication(indicationName: string) {
this.store.update((indications) => {
ViewportByteCategories.clearIndication(indications, indicationName)
return indications
})
}
public updateSearchIndications(
searchQuery: SearchData,
viewportFileOffset: number
) {
if (searchQuery.searchResults.length > 0) {
const resultsIterable =
searchQuery.iterableDataFromOffset(viewportFileOffset)
const { data } = resultsIterable
const start = data[0]
const byteLength = searchQuery.byteLength

let searchResultsHighlightLUT = new Uint8Array(1024).fill(0)
this.store.update((indications) => {
ViewportByteCategories.clearAndSetIf(
indications,
'searchresult',
(_, i) => {
const adjustIndex = i + viewportFileOffset
return adjustIndex >= start && adjustIndex < start + byteLength
}
)
searchResultsUpdated.set(true)
return indications
})
}
}

export enum HightlightCategoryMasks {
None = 0,
ActiveSelection = 1,
ConsideredForSelection = 2,
SearchResult = 4,
}
public updateReplaceIndications(
replaceData: ReplaceData,
viewportFileOffset: number
) {
const resultsIterable =
replaceData.iterableDataFromOffset(viewportFileOffset)

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 (resultsIterable.data.length > 0) {
const { offset, byteLength } = resultsIterable.data[0] as DataReplacement
this.store.update((indications) => {
ViewportByteCategories.clearAndSetIf(
indications,
'replacement',
(_, i) => {
const adjustIndex = i + viewportFileOffset
return adjustIndex >= offset && adjustIndex < offset + byteLength
}
)
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.clearIndication(indications, 'selected')
return indications
})
}
if (selectionData.active || selectionData.makingSelection()) {
const offsetPartitions = [
generateSelectionCategoryParition(0, start, (byte) => {
byte[0] &= ~category1.indexOf('selected')
}),
generateSelectionCategoryParition(start, editedEnd, (byte) => {
byte[0] |= category1.indexOf('selected')
}),
generateSelectionCategoryParition(
Math.max(originalEnd, editedEnd),
VIEWPORT_CAPACITY_MAX - start,
(byte) => {
byte[0] &= ~category1.indexOf('selected')
}
),
]
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
}

0 comments on commit d7f7088

Please sign in to comment.