mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 20:31:45 +03:00
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:
parent
590526a7a6
commit
38bbd8b6e6
@ -3,8 +3,11 @@
|
||||
#### Enso IDE
|
||||
|
||||
- ["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
|
||||
[10457]: https://github.com/enso-org/enso/pull/10457
|
||||
|
||||
#### Enso Enso Standard Library
|
||||
|
||||
|
@ -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)]
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
|
@ -6,7 +6,12 @@
|
||||
|
||||
import { createXXHash128 } from 'hash-wasm'
|
||||
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 { isNode } from '../util/detect'
|
||||
|
||||
@ -37,4 +42,4 @@ export async function initializeFFI(path?: string | undefined) {
|
||||
// await initializeFFI()
|
||||
|
||||
/* 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 }
|
||||
|
@ -12,10 +12,17 @@ declare global {
|
||||
function parse_tree(code: string): Uint8Array
|
||||
function parse_doc_to_json(docs: string): string
|
||||
function is_ident_or_operator(code: string): number
|
||||
function is_numeric_literal(code: string): boolean
|
||||
function xxHash128(input: IDataType): string
|
||||
}
|
||||
|
||||
export async function initializeFFI(_path?: string | undefined) {}
|
||||
|
||||
/* 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
|
||||
|
@ -32,6 +32,7 @@ import type { SourceRangeEdit } from '../util/data/text'
|
||||
import { allKeys } from '../util/types'
|
||||
import type { ExternalId, VisualizationMetadata } from '../yjsModel'
|
||||
import { visMetadataEquals } from '../yjsModel'
|
||||
import { is_numeric_literal } from './ffi'
|
||||
import * as RawAst from './generated/ast'
|
||||
import {
|
||||
applyTextEditsToAst,
|
||||
@ -1825,6 +1826,10 @@ export class MutableNumericLiteral extends NumericLiteral implements MutableAst
|
||||
export interface MutableNumericLiteral extends NumericLiteral, 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
|
||||
* GUI. We just need to represent them faithfully and create the simple cases. */
|
||||
type ArgumentDefinition<T extends TreeRefs = RawRefs> = (T['ast'] | T['token'])[]
|
||||
|
@ -10,9 +10,9 @@ import { computed, ref, type ComponentInstance } from 'vue'
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
const inputComponent = ref<ComponentInstance<typeof NumericInputWidget>>()
|
||||
|
||||
function setValue(value: number | string) {
|
||||
function setValue(value: string | undefined) {
|
||||
props.onUpdate({
|
||||
portUpdate: { value: value.toString(), origin: props.input.portId },
|
||||
portUpdate: { value, origin: props.input.portId },
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { usePointer } from '@/composables/events'
|
||||
import { isNumericLiteral } from 'shared/ast/tree'
|
||||
import { computed, ref, watch, type CSSProperties, type ComponentInstance } from 'vue'
|
||||
import AutoSizedInput from './AutoSizedInput.vue'
|
||||
|
||||
@ -9,7 +10,7 @@ const props = defineProps<{
|
||||
limits?: { min: number; max: number } | undefined
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [modelValue: number | string]
|
||||
'update:modelValue': [modelValue: string | undefined]
|
||||
blur: []
|
||||
focus: []
|
||||
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.
|
||||
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() : ''))
|
||||
watch(valueString, (newValue) => (editedValue.value = newValue), { immediate: true })
|
||||
const inputFieldActive = ref(false)
|
||||
@ -91,13 +100,14 @@ const inputStyle = computed<CSSProperties>(() => {
|
||||
})
|
||||
|
||||
function emitUpdate() {
|
||||
if (valueString.value !== editedValue.value) {
|
||||
emit('update:modelValue', editedValue.value)
|
||||
if (valueString.value !== lastValidValue.value) {
|
||||
emit('update:modelValue', lastValidValue.value == '' ? undefined : lastValidValue.value)
|
||||
}
|
||||
}
|
||||
|
||||
function blurred() {
|
||||
inputFieldActive.value = false
|
||||
editedValue.value = lastValidValue.value?.toString() ?? ''
|
||||
emit('blur')
|
||||
emitUpdate()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user