ast migrate + ensure ast highlights don't overlap

pull/12817/head
Ottomated 2 years ago
parent f3aa82eb2e
commit b8539aeb5c

@ -102,7 +102,8 @@
}
/** @type {StateEffectType<Range<Decoration>[]>} */
const addMarksDecoration = StateEffect.define();
const setMarksDecoration = StateEffect.define();
const clearMarksDecoration = StateEffect.define();
// This value must be added to the set of extensions to enable this
const markField = StateField.define({
@ -116,7 +117,11 @@
value = value.map(tr.changes);
// If this transaction adds or removes decorations, apply those changes
for (let effect of tr.effects) {
if (effect.is(addMarksDecoration)) value = value.update({ add: effect.value, sort: true });
if (effect.is(setMarksDecoration)) {
value = value.update({ filter: () => false, add: effect.value });
} else if (effect.is(clearMarksDecoration)) {
value = value.update({ filter: () => false });
}
}
return value;
},
@ -136,16 +141,13 @@
});
$cmInstance.view?.dispatch({
effects: [
StateEffect.appendConfig.of(markField),
addMarksDecoration.of([executedMark.range(from, to)])
]
effects: [setMarksDecoration.of([executedMark.range(from, to)])]
});
}
export function unmarkText() {
$cmInstance.view?.dispatch({
effects: StateEffect.reconfigure.of($cmInstance.extensions ?? [])
effects: [clearMarksDecoration.of(null)]
});
}
@ -220,7 +222,7 @@
lint: diagnostics,
lintOptions: { delay: 200 },
autocomplete: true,
extensions: [svelte_rune_completions, js_rune_completions, watcher],
extensions: [svelte_rune_completions, js_rune_completions, watcher, markField],
instanceStore: cmInstance
}}
oncodemirror:textChange={(/** @type {{ detail: string; }} */ event) => {

@ -1,98 +1,103 @@
<script>
import { get_repl_context } from '$lib/context.js';
import { tick } from 'svelte';
import { tick, untrack } from 'svelte';
export let key = '';
/** @type {import('svelte/types/compiler/interfaces').Ast} */
export let value;
export let collapsed = true;
/** @type {import('svelte/types/compiler/interfaces').Ast[]} */
export let path_nodes = [];
export let autoscroll = true;
/** @type {{ key?: string; value: unknown; collapsed?: boolean; path_nodes?: unknown[]; autoscroll?: boolean }} */
let { key = '', value, collapsed = true, path_nodes = [], autoscroll = true } = $props();
const { module_editor, toggleable } = get_repl_context();
/** @type {HTMLLIElement} */
let list_item_el;
$: is_root = path_nodes[0] === value;
$: is_leaf = path_nodes[path_nodes.length - 1] === value;
$: is_ast_array = Array.isArray(value);
$: is_collapsable = value && typeof value === 'object';
$: is_markable =
is_collapsable &&
'start' in value &&
'end' in value &&
typeof value.start === 'number' &&
typeof value.end === 'number';
$: key_text = key ? `${key}:` : '';
let preview_text = '';
$: {
if (!is_collapsable || !collapsed) break $;
const is_root = $derived(path_nodes[0] === value);
const is_leaf = $derived(path_nodes[path_nodes.length - 1] === value);
const is_ast_array = $derived(Array.isArray(value));
/**
* @param {unknown} value
* @returns {value is {}}
*/
function is_collapsable(value) {
return value !== null && typeof value === 'object';
}
const is_markable = $derived(
is_collapsable(value) &&
'start' in value &&
'end' in value &&
typeof value.start === 'number' &&
typeof value.end === 'number'
);
const key_text = key ? `${key}:` : '';
let preview_text = $state('');
$effect(() => {
if (!is_collapsable(value) || !collapsed) return;
if (is_ast_array) {
if (!('length' in value)) break $;
if (!('length' in value)) return;
preview_text = `[ ${value.length} element${value.length === 1 ? '' : 's'} ]`;
} else {
preview_text = `{ ${Object.keys(value).join(', ')} }`;
}
}
});
$: collapsed = !path_nodes.includes(value);
$effect(() => {
const collapse = !path_nodes.includes(value);
untrack(() => (collapsed = collapse));
});
$effect(() => {
if (!autoscroll || !is_leaf || $toggleable) return;
$: if (autoscroll && is_leaf && !$toggleable) {
// wait for all nodes to render before scroll
tick().then(() => {
if (list_item_el) {
list_item_el.scrollIntoView();
}
});
}
});
/** @param {MouseEvent | FocusEvent} e */
function handle_mark_text(e) {
if (is_markable) {
e.stopPropagation();
if (
'start' in value &&
'end' in value &&
typeof value.start === 'number' &&
typeof value.end === 'number'
) {
$module_editor?.markText({ from: value.start ?? 0, to: value.end ?? 0 });
}
if (!is_markable) return;
e.stopPropagation();
if (
is_collapsable(value) &&
'start' in value &&
'end' in value &&
typeof value.start === 'number' &&
typeof value.end === 'number'
) {
$module_editor?.markText({ from: value.start ?? 0, to: value.end ?? 0 });
}
}
/** @param {MouseEvent} e */
function handle_unmark_text(e) {
if (is_markable) {
e.stopPropagation();
$module_editor?.unmarkText();
}
if (!is_markable) return;
e.stopPropagation();
$module_editor?.unmarkText();
}
</script>
<li
bind:this={list_item_el}
class:marked={!is_root && is_leaf}
on:mouseover={handle_mark_text}
on:focus={handle_mark_text}
on:mouseleave={handle_unmark_text}
onmouseover={handle_mark_text}
onfocus={handle_mark_text}
onmouseleave={handle_unmark_text}
>
{#if !is_root && is_collapsable}
<button class="ast-toggle" class:open={!collapsed} on:click={() => (collapsed = !collapsed)}>
{#if !is_root && is_collapsable(value)}
<button class="ast-toggle" class:open={!collapsed} onclick={() => (collapsed = !collapsed)}>
{key_text}
</button>
{:else if key_text}
<span>{key_text}</span>
{/if}
{#if is_collapsable}
{#if is_collapsable(value)}
{#if collapsed && !is_root}
<button class="preview" on:click={() => (collapsed = !collapsed)}>
<button class="preview" onclick={() => (collapsed = !collapsed)}>
{preview_text}
</button>
{:else}

@ -3,20 +3,20 @@
import AstNode from './AstNode.svelte';
import { cursorIndex } from '../CodeMirror.svelte';
/** @type {import('svelte/types/compiler/interfaces').Ast} */
export let ast;
export let autoscroll = true;
/** @type {{ ast: unknown; autoscroll?: boolean }} */
const { ast, autoscroll = true } = $props();
// $cursor_index may go over the max since ast computation is usually slower.
// clamping this helps prevent the collapse view flashing
$: max_cursor_index = !ast ? $cursorIndex : Math.min($cursorIndex, get_ast_max_end(ast));
$: path_nodes = find_deepest_path(max_cursor_index, [ast]) || [];
const max_cursor_index = $derived(
!ast ? $cursorIndex : Math.min($cursorIndex, get_ast_max_end(ast))
);
const path_nodes = $derived(find_deepest_path(max_cursor_index, [ast]) || []);
/**
* @param {number} cursor
* @param {import('svelte/types/compiler/interfaces').Ast[]} paths
* @returns {import('svelte/types/compiler/interfaces').Ast[] | undefined}
* @param {unknown[]} paths
* @returns {unknown[] | undefined}
*/
function find_deepest_path(cursor, paths) {
const value = paths[paths.length - 1];
@ -31,6 +31,8 @@
}
if (
value !== null &&
typeof value === 'object' &&
'start' in value &&
'end' in value &&
typeof value.start === 'number' &&
@ -42,10 +44,12 @@
}
}
/** @param {import('svelte/types/compiler/interfaces').Ast} ast */
/** @param {unknown} ast */
function get_ast_max_end(ast) {
let max_end = 0;
if (ast === null || typeof ast !== 'object') return max_end;
for (const node of Object.values(ast)) {
if (node && typeof node.end === 'number' && node.end > max_end) {
max_end = node.end;

@ -1,4 +1,3 @@
<!-- svelte-ignore a11y-label-has-associated-control -->
<script>
import { get_repl_context } from '$lib/context.js';
import Checkbox from '../Checkbox.svelte';

@ -14,7 +14,7 @@
{#if error.start}
<button class="position" onclick={() => go_to_warning_pos(error)}>
<small>line {error.start.line} column {error.start.column}</small>
<small>line {error.start.line} column {error.start.column + 1}</small>
</button>
{/if}
</div>

@ -278,8 +278,6 @@
/** @type {import('./workers/workers').CompileMessageData | null} */
let compiled = $state(null);
$inspect(compiled);
/**
* @param {File | null} $selected
* @param {import('svelte/compiler').CompileOptions} $compile_options

Loading…
Cancel
Save