mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 00:52:09 +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
|
#### 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
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
|
@ -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'])[]
|
||||||
|
@ -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 },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user