enso/app/gui2/shared/ast/sourceDocument.ts
Kaz Wesley c811a5ae8b
Enable the Code Editor, with new apply-text-edits algo. (#9055)
- Fix the UI problems with our CodeMirror integration (Fixed view stability; Fixed a focus bug; Fixed errors caused by diagnostics range exceptions; Fixed linter invalidation--see https://discuss.codemirror.net/t/problem-trying-to-force-linting/5823; Implemented edit-coalescing for performance).
- Introduce an algorithm for applying text edits to an AST. Compared to the GUI1 approach, the new algorithm supports deeper identity-stability for expressions (which is important for subexpression metadata and Y.Js sync), as well as reordered-subtree identification.
- Enable the code editor.
2024-02-19 23:57:42 +00:00

94 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