Numeric input does not allow non-numeric value (#10457)

Fixes #9838 (see the "backlog refinement" comment there).

When someone want's to accept invalid input, it gets reverted to last recorded valid value.
This commit is contained in:
Adam Obuchowicz 2024-07-10 12:38:38 +02:00 committed by GitHub
parent 590526a7a6
commit 38bbd8b6e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 54 additions and 8 deletions

View File

@ -3,8 +3,11 @@
#### Enso IDE #### Enso IDE
- ["Add node" button is not obscured by output port][10433] - ["Add node" button is not obscured by output port][10433]
- [Numeric Widget does not accept non-numeric input][10457]. This is to prevent
node being completely altered by accidental code put to the widget.
[10433]: https://github.com/enso-org/enso/pull/10443 [10433]: https://github.com/enso-org/enso/pull/10443
[10457]: https://github.com/enso-org/enso/pull/10457
#### Enso Enso Standard Library #### Enso Enso Standard Library

View File

@ -37,6 +37,22 @@ pub fn is_ident_or_operator(code: &str) -> u32 {
} }
} }
#[wasm_bindgen]
pub fn is_numeric_literal(code: &str) -> bool {
let parsed = PARSER.with(|parser| parser.run(code));
let enso_parser::syntax::tree::Variant::BodyBlock(body) = *parsed.variant else { return false };
let [stmt] = &body.statements[..] else { return false };
stmt.expression.as_ref().map_or(false, |expr| match &*expr.variant {
enso_parser::syntax::tree::Variant::Number(_) => true,
enso_parser::syntax::tree::Variant::UnaryOprApp(app) =>
app.opr.code == "-"
&& app.rhs.as_ref().map_or(false, |rhs| {
matches!(*rhs.variant, enso_parser::syntax::tree::Variant::Number(_))
}),
_ => false,
})
}
#[wasm_bindgen(start)] #[wasm_bindgen(start)]
fn main() { fn main() {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();

View File

@ -6,7 +6,12 @@
import { createXXHash128 } from 'hash-wasm' import { createXXHash128 } from 'hash-wasm'
import type { IDataType } from 'hash-wasm/dist/lib/util' import type { IDataType } from 'hash-wasm/dist/lib/util'
import init, { is_ident_or_operator, parse, parse_doc_to_json } from '../../rust-ffi/pkg/rust_ffi' import init, {
is_ident_or_operator,
is_numeric_literal,
parse,
parse_doc_to_json,
} from '../../rust-ffi/pkg/rust_ffi'
import { assertDefined } from '../util/assert' import { assertDefined } from '../util/assert'
import { isNode } from '../util/detect' import { isNode } from '../util/detect'
@ -37,4 +42,4 @@ export async function initializeFFI(path?: string | undefined) {
// await initializeFFI() // await initializeFFI()
/* eslint-disable-next-line camelcase */ /* eslint-disable-next-line camelcase */
export { is_ident_or_operator, parse_doc_to_json, parse as parse_tree } export { is_ident_or_operator, is_numeric_literal, parse_doc_to_json, parse as parse_tree }

View File

@ -12,10 +12,17 @@ declare global {
function parse_tree(code: string): Uint8Array function parse_tree(code: string): Uint8Array
function parse_doc_to_json(docs: string): string function parse_doc_to_json(docs: string): string
function is_ident_or_operator(code: string): number function is_ident_or_operator(code: string): number
function is_numeric_literal(code: string): boolean
function xxHash128(input: IDataType): string function xxHash128(input: IDataType): string
} }
export async function initializeFFI(_path?: string | undefined) {} export async function initializeFFI(_path?: string | undefined) {}
/* eslint-disable-next-line camelcase */ /* eslint-disable-next-line camelcase */
export const { is_ident_or_operator, parse_doc_to_json, parse_tree, xxHash128 } = globalThis export const {
is_ident_or_operator,
is_numeric_literal,
parse_doc_to_json,
parse_tree,
xxHash128,
} = globalThis

View File

@ -32,6 +32,7 @@ import type { SourceRangeEdit } from '../util/data/text'
import { allKeys } from '../util/types' import { allKeys } from '../util/types'
import type { ExternalId, VisualizationMetadata } from '../yjsModel' import type { ExternalId, VisualizationMetadata } from '../yjsModel'
import { visMetadataEquals } from '../yjsModel' import { visMetadataEquals } from '../yjsModel'
import { is_numeric_literal } from './ffi'
import * as RawAst from './generated/ast' import * as RawAst from './generated/ast'
import { import {
applyTextEditsToAst, applyTextEditsToAst,
@ -1825,6 +1826,10 @@ export class MutableNumericLiteral extends NumericLiteral implements MutableAst
export interface MutableNumericLiteral extends NumericLiteral, MutableAst {} export interface MutableNumericLiteral extends NumericLiteral, MutableAst {}
applyMixins(MutableNumericLiteral, [MutableAst]) applyMixins(MutableNumericLiteral, [MutableAst])
export function isNumericLiteral(code: string) {
return is_numeric_literal(code)
}
/** The actual contents of an `ArgumentDefinition` are complex, but probably of more interest to the compiler than the /** The actual contents of an `ArgumentDefinition` are complex, but probably of more interest to the compiler than the
* GUI. We just need to represent them faithfully and create the simple cases. */ * GUI. We just need to represent them faithfully and create the simple cases. */
type ArgumentDefinition<T extends TreeRefs = RawRefs> = (T['ast'] | T['token'])[] type ArgumentDefinition<T extends TreeRefs = RawRefs> = (T['ast'] | T['token'])[]

View File

@ -10,9 +10,9 @@ import { computed, ref, type ComponentInstance } from 'vue'
const props = defineProps(widgetProps(widgetDefinition)) const props = defineProps(widgetProps(widgetDefinition))
const inputComponent = ref<ComponentInstance<typeof NumericInputWidget>>() const inputComponent = ref<ComponentInstance<typeof NumericInputWidget>>()
function setValue(value: number | string) { function setValue(value: string | undefined) {
props.onUpdate({ props.onUpdate({
portUpdate: { value: value.toString(), origin: props.input.portId }, portUpdate: { value, origin: props.input.portId },
}) })
} }

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { usePointer } from '@/composables/events' import { usePointer } from '@/composables/events'
import { isNumericLiteral } from 'shared/ast/tree'
import { computed, ref, watch, type CSSProperties, type ComponentInstance } from 'vue' import { computed, ref, watch, type CSSProperties, type ComponentInstance } from 'vue'
import AutoSizedInput from './AutoSizedInput.vue' import AutoSizedInput from './AutoSizedInput.vue'
@ -9,7 +10,7 @@ const props = defineProps<{
limits?: { min: number; max: number } | undefined limits?: { min: number; max: number } | undefined
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
'update:modelValue': [modelValue: number | string] 'update:modelValue': [modelValue: string | undefined]
blur: [] blur: []
focus: [] focus: []
input: [content: string] input: [content: string]
@ -21,6 +22,14 @@ const MIN_CONTENT_WIDTH = 56
// Edited value reflects the `modelValue`, but does not update it until the user defocuses the field. // Edited value reflects the `modelValue`, but does not update it until the user defocuses the field.
const editedValue = ref('') const editedValue = ref('')
// Last value which is a parseable number. It's a string, because the Enso number literals differ from js
// representations.
const lastValidValue = ref<string>()
watch(editedValue, (newValue) => {
if (newValue == '' || isNumericLiteral(newValue)) {
lastValidValue.value = newValue
}
})
const valueString = computed(() => (props.modelValue != null ? props.modelValue.toString() : '')) const valueString = computed(() => (props.modelValue != null ? props.modelValue.toString() : ''))
watch(valueString, (newValue) => (editedValue.value = newValue), { immediate: true }) watch(valueString, (newValue) => (editedValue.value = newValue), { immediate: true })
const inputFieldActive = ref(false) const inputFieldActive = ref(false)
@ -91,13 +100,14 @@ const inputStyle = computed<CSSProperties>(() => {
}) })
function emitUpdate() { function emitUpdate() {
if (valueString.value !== editedValue.value) { if (valueString.value !== lastValidValue.value) {
emit('update:modelValue', editedValue.value) emit('update:modelValue', lastValidValue.value == '' ? undefined : lastValidValue.value)
} }
} }
function blurred() { function blurred() {
inputFieldActive.value = false inputFieldActive.value = false
editedValue.value = lastValidValue.value?.toString() ?? ''
emit('blur') emit('blur')
emitUpdate() emitUpdate()
} }