From 44dee50d9f2633b4290e18aba1373caf0427864b Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Tue, 24 Oct 2023 16:44:19 +0200 Subject: [PATCH 1/2] Add type tags for number ranges --- src/renderer/components/TypeTag.tsx | 197 +++++++++++++++++++++++----- 1 file changed, 166 insertions(+), 31 deletions(-) diff --git a/src/renderer/components/TypeTag.tsx b/src/renderer/components/TypeTag.tsx index 16297666a..d5d2538c0 100644 --- a/src/renderer/components/TypeTag.tsx +++ b/src/renderer/components/TypeTag.tsx @@ -1,13 +1,20 @@ import { + Bounds, + IntIntervalType, + IntervalType, NeverType, Type, + intInterval, isNumericLiteral, isStringLiteral, isStructInstance, + isSubsetOf, + literal, } from '@chainner/navi'; import { Tag, Tooltip, forwardRef } from '@chakra-ui/react'; -import React, { memo } from 'react'; +import React, { ReactNode, memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { explain } from '../../common/types/explain'; import { getFields, isColor, isDirectory, isImage, withoutNull } from '../../common/types/util'; import { assertNever } from '../../common/util'; @@ -24,8 +31,87 @@ const getColorMode = (channels: number) => { } }; +const getSimplifiedNumberRange = (type: Type): IntIntervalType | IntervalType | undefined => { + if (type.underlying === 'number') { + if (type.type === 'interval' || type.type === 'int-interval') { + return type; + } + if (type.type === 'non-int-interval') { + if (Number.isFinite(type.min) && Number.isFinite(type.max)) { + return new IntervalType(type.min, type.max, Bounds.Inclusive); + } + return new IntervalType(type.min, type.max, Bounds.Exclusive); + } + } + if (type.underlying === 'union') { + let min = Infinity; + let max = -Infinity; + + for (const item of type.items) { + if (item.underlying === 'number') { + if (item.type === 'literal') { + const { value } = item; + if (Number.isNaN(value)) { + // we don't deal with nan + return undefined; + } + min = Math.min(min, value); + max = Math.max(max, value); + } else if (item.type === 'number') { + // we don't deal with all numbers + return undefined; + } else { + min = Math.min(min, item.min); + max = Math.max(max, item.max); + } + } else { + return undefined; + } + } + + if (min < max) { + if (isSubsetOf(type, intInterval(-Infinity, Infinity))) { + return new IntIntervalType(min, max); + } + return new IntervalType(min, max, Bounds.Inclusive); + } + } +}; + +const collectNumericLiterals = (type: Type, maximum = 4): number[] | undefined => { + const set = new Set(); + + const items = type.underlying === 'union' ? type.items : [type]; + for (const item of items) { + if (item.underlying === 'number') { + if (item.type === 'literal') { + set.add(item.value); + if (set.size > maximum) { + return undefined; + } + } else if (item.type === 'int-interval') { + const count = item.max - item.min + 1; + if (count + set.size > maximum) { + return undefined; + } + for (let i = item.min; i <= item.max; i += 1) { + set.add(i); + } + } else { + return undefined; + } + } else { + return undefined; + } + } + + const list = [...set]; + list.sort((a, b) => a - b); + return list; +}; + type TagValue = - | { kind: 'literal'; value: string } + | { kind: 'literal'; value: string; tooltip?: string } | { kind: 'string'; value: string } | { kind: 'path'; value: string }; @@ -33,6 +119,47 @@ const getTypeText = (type: Type): TagValue[] => { if (isNumericLiteral(type)) return [{ kind: 'literal', value: type.toString() }]; if (isStringLiteral(type)) return [{ kind: 'string', value: type.value }]; + const numberLiterals = collectNumericLiterals(type, 4); + if (numberLiterals) { + return [ + { + kind: 'literal', + value: numberLiterals.map((l) => literal(l).toString()).join(' | '), + }, + ]; + } + + const rangeType = getSimplifiedNumberRange(type); + if (rangeType) { + const tooltip = explain(rangeType, { detailed: true }); + if (rangeType.type === 'interval') { + const { min, max } = rangeType; + if (Number.isFinite(min) && Number.isFinite(max)) { + return [{ kind: 'literal', value: `${min} ~ ${max}`, tooltip }]; + } + if (Number.isFinite(min) && max === Infinity) { + const op = rangeType.minExclusive ? '>' : '>='; + return [{ kind: 'literal', value: `${op} ${min}`, tooltip }]; + } + if (min === -Infinity && Number.isFinite(max)) { + const op = rangeType.maxExclusive ? '<' : '<='; + return [{ kind: 'literal', value: `${op} ${max}`, tooltip }]; + } + } + if (rangeType.type === 'int-interval') { + const { min, max } = rangeType; + if (Number.isFinite(min) && Number.isFinite(max)) { + return [{ kind: 'literal', value: `int ${min} ~ ${max}`, tooltip }]; + } + if (Number.isFinite(min) && max === Infinity) { + return [{ kind: 'literal', value: `int >= ${min}`, tooltip }]; + } + if (min === -Infinity && Number.isFinite(max)) { + return [{ kind: 'literal', value: `int <= ${max}`, tooltip }]; + } + } + } + const tags: TagValue[] = []; if (isImage(type)) { const { width, height, channels } = getFields(type); @@ -148,49 +275,57 @@ const Punctuation = memo(({ children }: React.PropsWithChildren) => { const TagRenderer = memo(({ tag }: { tag: TagValue }) => { const { kind, value } = tag; + let tt: string | undefined; + let text: NonNullable; + switch (kind) { case 'path': { + tt = value; const maxLength = 14; - return ( - - - {value.length > maxLength && } - {value.slice(Math.max(0, value.length - maxLength))} - - + text = ( + <> + {value.length > maxLength && } + {value.slice(Math.max(0, value.length - maxLength))} + ); + break; } case 'string': { + tt = value; const maxLength = 16; - return ( - - - {value.slice(0, maxLength)} - {value.length > maxLength && } - - + text = ( + <> + {value.slice(0, maxLength)} + {value.length > maxLength && } + ); + break; } case 'literal': { - return {value}; + tt = tag.tooltip; + text = value; + break; } default: return assertNever(kind); } + + if (!tt) { + return {text}; + } + + return ( + + {text} + + ); }); export const TypeTags = memo(({ type, isOptional }: TypeTagsProps) => { From 45c98d8c7ff99b2134a4b2ae0063e5965c1e4d90 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 25 Oct 2023 13:15:17 +0200 Subject: [PATCH 2/2] Improved range formatting --- src/renderer/components/TypeTag.tsx | 51 ++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/src/renderer/components/TypeTag.tsx b/src/renderer/components/TypeTag.tsx index d5d2538c0..1ea2b4a63 100644 --- a/src/renderer/components/TypeTag.tsx +++ b/src/renderer/components/TypeTag.tsx @@ -9,7 +9,6 @@ import { isStringLiteral, isStructInstance, isSubsetOf, - literal, } from '@chainner/navi'; import { Tag, Tooltip, forwardRef } from '@chakra-ui/react'; import React, { ReactNode, memo } from 'react'; @@ -110,6 +109,21 @@ const collectNumericLiterals = (type: Type, maximum = 4): number[] | undefined = return list; }; +const formatNumber = (n: number): string => { + if (Number.isNaN(n)) return 'nan'; + if (n === Infinity) return 'inf'; + if (n === -Infinity) return '-inf'; + if (Number.isInteger(n)) return n.toString(); + + const dotPosition = Math.ceil(Math.log10(Math.abs(n))); + const digits = Math.max(dotPosition + 2, 5); + if (digits < 18) { + // eslint-disable-next-line no-param-reassign + n = Number(n.toExponential(digits)); + } + return n.toString(); +}; + type TagValue = | { kind: 'literal'; value: string; tooltip?: string } | { kind: 'string'; value: string } @@ -121,12 +135,7 @@ const getTypeText = (type: Type): TagValue[] => { const numberLiterals = collectNumericLiterals(type, 4); if (numberLiterals) { - return [ - { - kind: 'literal', - value: numberLiterals.map((l) => literal(l).toString()).join(' | '), - }, - ]; + return [{ kind: 'literal', value: numberLiterals.map(formatNumber).join(' | ') }]; } const rangeType = getSimplifiedNumberRange(type); @@ -135,27 +144,39 @@ const getTypeText = (type: Type): TagValue[] => { if (rangeType.type === 'interval') { const { min, max } = rangeType; if (Number.isFinite(min) && Number.isFinite(max)) { - return [{ kind: 'literal', value: `${min} ~ ${max}`, tooltip }]; + return [ + { + kind: 'literal', + value: `${formatNumber(min)}..${formatNumber(max)}`, + tooltip, + }, + ]; } if (Number.isFinite(min) && max === Infinity) { - const op = rangeType.minExclusive ? '>' : '>='; - return [{ kind: 'literal', value: `${op} ${min}`, tooltip }]; + const op = rangeType.minExclusive ? '>' : '≥'; + return [{ kind: 'literal', value: `${op} ${formatNumber(min)}`, tooltip }]; } if (min === -Infinity && Number.isFinite(max)) { - const op = rangeType.maxExclusive ? '<' : '<='; - return [{ kind: 'literal', value: `${op} ${max}`, tooltip }]; + const op = rangeType.maxExclusive ? '<' : '≤'; + return [{ kind: 'literal', value: `${op} ${formatNumber(max)}`, tooltip }]; } } if (rangeType.type === 'int-interval') { const { min, max } = rangeType; if (Number.isFinite(min) && Number.isFinite(max)) { - return [{ kind: 'literal', value: `int ${min} ~ ${max}`, tooltip }]; + return [ + { + kind: 'literal', + value: `int ${formatNumber(min)}..${formatNumber(max)}`, + tooltip, + }, + ]; } if (Number.isFinite(min) && max === Infinity) { - return [{ kind: 'literal', value: `int >= ${min}`, tooltip }]; + return [{ kind: 'literal', value: `int ≥ ${formatNumber(min)}`, tooltip }]; } if (min === -Infinity && Number.isFinite(max)) { - return [{ kind: 'literal', value: `int <= ${max}`, tooltip }]; + return [{ kind: 'literal', value: `int ≤ ${formatNumber(max)}`, tooltip }]; } } }