enso/app/gui2/shared/ast/sourceDocument.ts
Paweł Grabarz b7a8909818
Vue dependency update, better selection performance, visible quotes in text inputs (#9204)
- Improved performance by batching simulatenous node edits, including metadata updates when dragging many selected nodes together.
- Updated Vue to new version, allowing us to use `defineModel`.
- Fixed #9161
- Unified all handling of auto-blur by making `useAutoBlur` cheap to register - all logic goes through a single window event handler.
- Combined all `ResizeObserver`s into one.
- Fixed the behaviour of repeated toast messages. Now only the latest compilation status is visible at any given time, and the errors disappear once compilation passes.
- Actually fixed broken interaction of node and visualization widths. There no longer is a style feedback loop and the visible node backdrop width no longer jumps or randomly fails to update.
2024-03-06 15:34:07 +00:00

95 lines
3.1 KiB
TypeScript

import { print, type AstId, type Module, type ModuleUpdate } from '.'
import { assertDefined } from '../util/assert'
import type { SourceRangeEdit } from '../util/data/text'
import { offsetEdit, textChangeToEdits } from '../util/data/text'
import type { Origin, SourceRange } from '../yjsModel'
import { rangeEquals, sourceRangeFromKey } from '../yjsModel'
/** Provides a view of the text representation of a module,
* and information about the correspondence between the text and the ASTs,
* that can be kept up-to-date by applying AST changes.
*/
export class SourceDocument {
private text_: string
private readonly spans: Map<AstId, SourceRange>
private readonly observers: SourceDocumentObserver[]
private constructor(text: string, spans: Map<AstId, SourceRange>) {
this.text_ = text
this.spans = spans
this.observers = []
}
static Empty() {
return new this('', new Map())
}
clear() {
if (this.spans.size !== 0) this.spans.clear()
if (this.text_ !== '') {
const range: SourceRange = [0, this.text_.length]
this.text_ = ''
this.notifyObservers([{ range, insert: '' }], undefined)
}
}
applyUpdate(module: Module, update: ModuleUpdate) {
for (const id of update.nodesDeleted) this.spans.delete(id)
const root = module.root()
if (!root) return
const subtreeTextEdits = new Array<SourceRangeEdit>()
const printed = print(root)
for (const [key, nodes] of printed.info.nodes) {
const range = sourceRangeFromKey(key)
for (const node of nodes) {
const oldSpan = this.spans.get(node.id)
if (!oldSpan || !rangeEquals(range, oldSpan)) this.spans.set(node.id, range)
if (update.updateRoots.has(node.id) && node.id !== root.id) {
assertDefined(oldSpan)
const oldCode = this.text_.slice(oldSpan[0], oldSpan[1])
const newCode = printed.code.slice(range[0], range[1])
const subedits = textChangeToEdits(oldCode, newCode).map((textEdit) =>
offsetEdit(textEdit, oldSpan[0]),
)
subtreeTextEdits.push(...subedits)
}
}
}
if (printed.code !== this.text_) {
const textEdits =
update.updateRoots.has(root.id) ?
[{ range: [0, this.text_.length] satisfies SourceRange, insert: printed.code }]
: subtreeTextEdits
this.text_ = printed.code
this.notifyObservers(textEdits, update.origin)
}
}
get text(): string {
return this.text_
}
getSpan(id: AstId): SourceRange | undefined {
return this.spans.get(id)
}
observe(observer: SourceDocumentObserver) {
this.observers.push(observer)
if (this.text_.length) observer([{ range: [0, 0], insert: this.text_ }], undefined)
}
unobserve(observer: SourceDocumentObserver) {
const index = this.observers.indexOf(observer)
if (index !== undefined) this.observers.splice(index, 1)
}
private notifyObservers(textEdits: SourceRangeEdit[], origin: Origin | undefined) {
for (const o of this.observers) o(textEdits, origin)
}
}
export type SourceDocumentObserver = (
textEdits: SourceRangeEdit[],
origin: Origin | undefined,
) => void