mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 00:52:09 +03:00
c811a5ae8b
- 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.
94 lines
3.1 KiB
TypeScript
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
|