Skip to content
This repository was archived by the owner on Mar 3, 2023. It is now read-only.

Commit 815cd2b

Browse files
committed
Add randomized test for updating syntax highlighting, fix bugs
1 parent bc061bb commit 815cd2b

File tree

2 files changed

+126
-11
lines changed

2 files changed

+126
-11
lines changed

spec/tree-sitter-language-mode-spec.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
22

3+
const fs = require('fs')
4+
const path = require('path')
35
const dedent = require('dedent')
46
const TextBuffer = require('text-buffer')
57
const {Point} = TextBuffer
68
const TextEditor = require('../src/text-editor')
79
const TreeSitterGrammar = require('../src/tree-sitter-grammar')
810
const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode')
11+
const Random = require('../script/node_modules/random-seed')
12+
const {getRandomBufferRange, buildRandomLines} = require('./helpers/random')
913

1014
const cGrammarPath = require.resolve('language-c/grammars/tree-sitter-c.cson')
1115
const pythonGrammarPath = require.resolve('language-python/grammars/tree-sitter-python.cson')
@@ -789,6 +793,97 @@ describe('TreeSitterLanguageMode', () => {
789793
})
790794
})
791795

796+
describe('highlighting after random changes', () => {
797+
let originalTimeout
798+
799+
beforeEach(() => {
800+
originalTimeout = jasmine.getEnv().defaultTimeoutInterval
801+
jasmine.getEnv().defaultTimeoutInterval = 60 * 1000
802+
})
803+
804+
afterEach(() => {
805+
jasmine.getEnv().defaultTimeoutInterval = originalTimeout
806+
})
807+
808+
it('matches the highlighting of a freshly-opened editor', async () => {
809+
jasmine.useRealClock()
810+
811+
const text = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8')
812+
atom.grammars.loadGrammarSync(jsGrammarPath)
813+
atom.grammars.assignLanguageMode(buffer, 'source.js')
814+
buffer.getLanguageMode().syncOperationLimit = 0
815+
816+
const initialSeed = Date.now()
817+
for (let i = 0, trial_count = 10; i < trial_count; i++) {
818+
let seed = initialSeed + i
819+
// seed = 1541201470759
820+
const random = Random(seed)
821+
822+
// Parse the initial content and render all of the screen lines.
823+
buffer.setText(text)
824+
buffer.clearUndoStack()
825+
await buffer.getLanguageMode().parseCompletePromise()
826+
editor.displayLayer.getScreenLines()
827+
828+
// Make several random edits.
829+
for (let j = 0, edit_count = 1 + random(4); j < edit_count; j++) {
830+
const editRoll = random(10)
831+
const range = getRandomBufferRange(random, buffer)
832+
833+
if (editRoll < 2) {
834+
const linesToInsert = buildRandomLines(random, range.getExtent().row + 1)
835+
// console.log('replace', range.toString(), JSON.stringify(linesToInsert))
836+
buffer.setTextInRange(range, linesToInsert)
837+
} else if (editRoll < 5) {
838+
// console.log('delete', range.toString())
839+
buffer.delete(range)
840+
} else {
841+
const linesToInsert = buildRandomLines(random, 3)
842+
// console.log('insert', range.start.toString(), JSON.stringify(linesToInsert))
843+
buffer.insert(range.start, linesToInsert)
844+
}
845+
846+
// console.log(buffer.getText())
847+
848+
// Sometimes, let the parse complete before re-rendering.
849+
// Sometimes re-render and move on before the parse completes.
850+
if (random(2)) await buffer.getLanguageMode().parseCompletePromise()
851+
editor.displayLayer.getScreenLines()
852+
}
853+
854+
// Revert the edits, because Tree-sitter's error recovery is somewhat path-dependent,
855+
// and we want a state where the tree parse result is guaranteed.
856+
while (buffer.undo()) {}
857+
858+
// Create a fresh buffer and editor with the same text.
859+
const buffer2 = new TextBuffer(buffer.getText())
860+
const editor2 = new TextEditor({buffer: buffer2})
861+
atom.grammars.assignLanguageMode(buffer2, 'source.js')
862+
863+
// Verify that the the two buffers have the same syntax highlighting.
864+
await buffer.getLanguageMode().parseCompletePromise()
865+
await buffer2.getLanguageMode().parseCompletePromise()
866+
expect(buffer.getLanguageMode().tree.rootNode.toString()).toEqual(
867+
buffer2.getLanguageMode().tree.rootNode.toString(), `Seed: ${seed}`
868+
)
869+
870+
for (let j = 0, n = editor.getScreenLineCount(); j < n; j++) {
871+
const tokens1 = editor.tokensForScreenRow(j)
872+
const tokens2 = editor2.tokensForScreenRow(j)
873+
expect(tokens1).toEqual(tokens2, `Seed: ${seed}, screen line: ${j}`)
874+
if (jasmine.getEnv().currentSpec.results().failedCount > 0) {
875+
console.log(tokens1)
876+
console.log(tokens2)
877+
debugger
878+
break
879+
}
880+
}
881+
882+
if (jasmine.getEnv().currentSpec.results().failedCount > 0) break
883+
}
884+
})
885+
})
886+
792887
describe('folding', () => {
793888
it('can fold nodes that start and end with specified tokens', async () => {
794889
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {

src/tree-sitter-language-mode.js

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class TreeSitterLanguageMode {
4545
this.hasQueuedParse = false
4646

4747
this.grammarForLanguageString = this.grammarForLanguageString.bind(this)
48-
this.emitRangeUpdate = this.emitRangeUpdate.bind(this)
4948

5049
this.subscription = this.buffer.onDidChangeText(({changes}) => {
5150
for (let i = 0, {length} = changes; i < length; i++) {
@@ -70,6 +69,25 @@ class TreeSitterLanguageMode {
7069
this.regexesByPattern = {}
7170
}
7271

72+
async parseCompletePromise () {
73+
let done = false
74+
while (!done) {
75+
if (this.rootLanguageLayer.currentParsePromise) {
76+
await this.rootLanguageLayer.currentParsePromises
77+
} else {
78+
done = true
79+
for (const marker of this.injectionsMarkerLayer.getMarkers()) {
80+
if (marker.languageLayer.currentParsePromise) {
81+
done = false
82+
await marker.languageLayer.currentParsePromise
83+
break
84+
}
85+
}
86+
}
87+
await new Promise(resolve => setTimeout(resolve, 0))
88+
}
89+
}
90+
7391
destroy () {
7492
this.injectionsMarkerLayer.destroy()
7593
this.subscription.dispose()
@@ -548,8 +566,8 @@ class LanguageLayer {
548566
if (this.patchSinceCurrentParseStarted) {
549567
this.patchSinceCurrentParseStarted.splice(
550568
oldRange.start,
551-
oldRange.end,
552-
newRange.end,
569+
oldRange.getExtent(),
570+
newRange.getExtent(),
553571
oldText,
554572
newText
555573
)
@@ -613,10 +631,14 @@ class LanguageLayer {
613631

614632
const changes = this.patchSinceCurrentParseStarted.getChanges()
615633
this.patchSinceCurrentParseStarted = null
616-
for (let i = changes.length - 1; i >= 0; i--) {
617-
const {oldStart, oldEnd, newEnd, oldText, newText} = changes[i]
634+
for (const {oldStart, newStart, oldEnd, newEnd, oldText, newText} of changes) {
635+
const newExtent = Point.fromObject(newEnd).traversalFrom(newStart)
618636
tree.edit(this._treeEditForBufferChange(
619-
oldStart, oldEnd, newEnd, oldText, newText
637+
newStart,
638+
oldEnd,
639+
Point.fromObject(oldStart).traverse(newExtent),
640+
oldText,
641+
newText
620642
))
621643
}
622644

@@ -655,9 +677,7 @@ class LanguageLayer {
655677
}
656678

657679
_populateInjections (range, nodeRangeSet) {
658-
const {injectionsMarkerLayer, grammarForLanguageString} = this.languageMode
659-
660-
const existingInjectionMarkers = injectionsMarkerLayer
680+
const existingInjectionMarkers = this.languageMode.injectionsMarkerLayer
661681
.findMarkers({intersectsRange: range})
662682
.filter(marker => marker.parentLanguageLayer === this)
663683

@@ -680,7 +700,7 @@ class LanguageLayer {
680700
const languageName = injectionPoint.language(node)
681701
if (!languageName) continue
682702

683-
const grammar = grammarForLanguageString(languageName)
703+
const grammar = this.languageMode.grammarForLanguageString(languageName)
684704
if (!grammar) continue
685705

686706
const contentNodes = injectionPoint.content(node)
@@ -695,7 +715,7 @@ class LanguageLayer {
695715
m.languageLayer.grammar === grammar
696716
)
697717
if (!marker) {
698-
marker = injectionsMarkerLayer.markRange(injectionRange)
718+
marker = this.languageMode.injectionsMarkerLayer.markRange(injectionRange)
699719
marker.languageLayer = new LanguageLayer(this.languageMode, grammar, injectionPoint.contentChildTypes)
700720
marker.parentLanguageLayer = this
701721
}

0 commit comments

Comments
 (0)