From 8e5821e842c0386b0e7e1cd06ebe706ab5c12145 Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Sat, 18 Aug 2018 17:50:25 -0500 Subject: [PATCH 01/11] add a defensive check before setting the cursor --- src/parinfer.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/parinfer.js b/src/parinfer.js index cc27229..607d8d0 100644 --- a/src/parinfer.js +++ b/src/parinfer.js @@ -72,9 +72,11 @@ function applyParinfer2 (editor, inputText, opts, mode) { editPromise.then(function (editWasApplied) { if (editWasApplied) { // set the new cursor position - const newCursorPosition = new Position(result.cursorLine, result.cursorX) - const nextCursor = new Selection(newCursorPosition, newCursorPosition) - editor.selection = nextCursor + if (Number.isInteger(result.cursorLine) && Number.isInteger(result.cursorX)) { + const newCursorPosition = new Position(result.cursorLine, result.cursorX) + const nextCursor = new Selection(newCursorPosition, newCursorPosition) + editor.selection = nextCursor + } updateParenTrails(mode, editor, result.parenTrails) } else { From 9a6ec69b646e9269e7fbc56b9dc2f7a128c4810a Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Sat, 18 Aug 2018 21:05:59 -0500 Subject: [PATCH 02/11] WIP --- src/extension.js | 113 ++++++++++++++++++++++------------------------- src/parinfer.js | 7 ++- src/util.js | 15 ++++++- 3 files changed, 73 insertions(+), 62 deletions(-) diff --git a/src/extension.js b/src/extension.js index 3a64294..1a4e7d7 100644 --- a/src/extension.js +++ b/src/extension.js @@ -4,20 +4,15 @@ const vscode = require('vscode') const window = vscode.window - const statusBar = require('./statusbar') - const editorModule = require('./editor') const editorStates = editorModule.editorStates - const parenTrailsModule = require('./parentrails') const clearParenTrailDecorators = parenTrailsModule.clearParenTrailDecorators - const parinfer2 = require('./parinfer') const util = require('./util') - +const debounce = util.debounce const config = require('./config') - const path = require('path') const fs = require('fs') @@ -27,7 +22,6 @@ const fs = require('fs') const documentChangeEvent = 'DOCUMENT_CHANGE' const selectionChangeEvent = 'SELECTION_CHANGE' -const fiveSecondsMs = 5 * 1000 // ----------------------------------------------------------------------------- // Events Queue @@ -35,23 +29,15 @@ const fiveSecondsMs = 5 * 1000 // TODO: need documentation for how the eventsQueue works let eventsQueue = [] - -const eventsQueueMaxLength = 10 - -function cleanUpEventsQueue () { - if (eventsQueue.length > eventsQueueMaxLength) { - eventsQueue.length = eventsQueueMaxLength - } -} - -setInterval(cleanUpEventsQueue, fiveSecondsMs) +let prevCursorLine = null +let prevCursorX = null +let prevTxt = null const logEventsQueue = false function processEventsQueue () { - // defensive: these should never happen + // do nothing if the queue is empty if (eventsQueue.length === 0) return - if (eventsQueue[0].type !== selectionChangeEvent) return const activeEditor = window.activeTextEditor const editorMode = editorStates.deref().get(activeEditor) @@ -60,13 +46,15 @@ function processEventsQueue () { if (!util.isRunState(editorMode)) return if (logEventsQueue) { + if (eventsQueue[3]) console.log('3: ', eventsQueue[3]) if (eventsQueue[2]) console.log('2: ', eventsQueue[2]) if (eventsQueue[1]) console.log('1: ', eventsQueue[1]) console.log('0: ', eventsQueue[0]) - console.log('~~~~~~~~~~~~~~~~ eventsQueue ~~~~~~~~~~~~~~~~') } - const txt = eventsQueue[0].txt + // current text / previous text + const currentTxt = eventsQueue[0].txt + prevTxt = currentTxt // cursor options let options = { @@ -74,30 +62,33 @@ function processEventsQueue () { cursorX: eventsQueue[0].cursorX } - // TODO: this is not working correctly - // // add selectionStartLine if applicable - // if (eventsQueue[0].selectionStartLine) { - // options.selectionStartLine = eventsQueue[0].selectionStartLine - // } - - // check the last two events for previous cursor information - if (eventsQueue[1] && eventsQueue[1].cursorLine) { - options.prevCursorLine = eventsQueue[1].cursorLine - options.prevCursorX = eventsQueue[1].cursorX - } else if (eventsQueue[2] && eventsQueue[2].cursorLine) { - options.prevCursorLine = eventsQueue[2].cursorLine - options.prevCursorX = eventsQueue[2].cursorX + // grab the document changes + if (eventsQueue[0].type === documentChangeEvent && eventsQueue[0].changes) { + options.changes = eventsQueue[0].changes + } else if (eventsQueue[0].type === selectionChangeEvent && + eventsQueue[1] && + eventsQueue[1].type === documentChangeEvent && + eventsQueue[1].changes) { + options.changes = eventsQueue[1].changes } - // "document change" events always fire first followed immediately by a "selection change" event - // try to grab the changes from the most recent document change event - if (eventsQueue[1] && eventsQueue[1].type === documentChangeEvent && eventsQueue[1].changes) { - options.changes = eventsQueue[1].changes + // previous cursor information + options.prevCursorLine = prevCursorLine + options.prevCursorX = prevCursorX + prevCursorLine = options.cursorLine + prevCursorX = options.cursorX + + if (logEventsQueue) { + console.log('Parinfer options: ' + JSON.stringify(options)) + console.log('~~~~~~~~~~~~~~~~ eventsQueue ~~~~~~~~~~~~~~~~') } - parinfer2.applyParinfer(activeEditor, txt, options) + eventsQueue.length = 0 + parinfer2.applyParinfer(activeEditor, currentTxt, options) } +const debouncedProcessEventsQueue = debounce(processEventsQueue, 5) + // ----------------------------------------------------------------------------- // Change Editor State // ----------------------------------------------------------------------------- @@ -131,8 +122,11 @@ editorStates.addWatch(onChangeEditorStates) // ----------------------------------------------------------------------------- function onChangeActiveEditor (editor) { - // clear out the eventsQueue when we switch editor tabs - eventsQueue = [] + // clear out the state when we switch the active editor + eventsQueue.length = 0 + prevCursorLine = null + prevCursorX = null + prevTxt = null if (editor) { parinfer2.helloEditor(editor) @@ -149,50 +143,51 @@ function convertChangeObjects (oldTxt, changeEvt) { } } +// this function fires any time a document's content is changed function onChangeTextDocument (evt) { // drop any events that do not contain document changes - // (usually the first event to a document) + // NOTE: this is usually the first change to a document and anytime the user presses "save" if (evt.contentChanges && evt.contentChanges.length === 0) { return } + const activeEditor = window.activeTextEditor + const theDocument = evt.document let parinferEvent = { - txt: evt.document.getText(), + cursorLine: activeEditor.selections[0].active.line, + cursorX: activeEditor.selections[0].active.character, + documentVersion: theDocument.version, + txt: theDocument.getText(), type: documentChangeEvent } - // only create a "changes" property if we have a prior event - if (eventsQueue[0] && eventsQueue[0].txt) { - const prevTxt = eventsQueue[0].txt + // only create a "changes" property if there was prior text + if (prevTxt) { const convertFn = convertChangeObjects.bind(null, prevTxt) parinferEvent.changes = evt.contentChanges.map(convertFn) } - // put this event on the queue + // put this event on the queue and schedule a processing eventsQueue.unshift(parinferEvent) + debouncedProcessEventsQueue() } +// this function fires any time a cursor's position changes (ie: often) function onChangeSelection (evt) { const editor = evt.textEditor + const theDocument = editor.document const selection = evt.selections[0] - let parinferEvent = { + const parinferEvent = { cursorLine: selection.active.line, cursorX: selection.active.character, - txt: editor.document.getText(), + documentVersion: theDocument.version, + txt: theDocument.getText(), type: selectionChangeEvent } - // TODO: this is not working correctly - // // add selectionStartLine if applicable - // if (selection && !selection.isEmpty) { - // parinferEvent.selectionStartLine = selection.start.line - // } - - // put this event on the queue + // put this event on the queue and schedule a processing eventsQueue.unshift(parinferEvent) - - // process the queue after every "selection change" event - processEventsQueue() + debouncedProcessEventsQueue() } // ----------------------------------------------------------------------------- diff --git a/src/parinfer.js b/src/parinfer.js index 607d8d0..0a19def 100644 --- a/src/parinfer.js +++ b/src/parinfer.js @@ -72,10 +72,13 @@ function applyParinfer2 (editor, inputText, opts, mode) { editPromise.then(function (editWasApplied) { if (editWasApplied) { // set the new cursor position - if (Number.isInteger(result.cursorLine) && Number.isInteger(result.cursorX)) { + // NOTE: ignore the cursor from Parinfer if the user has multiple cursors + if (Number.isInteger(result.cursorLine) && + Number.isInteger(result.cursorX) && + editor.selections.length <= 1) { const newCursorPosition = new Position(result.cursorLine, result.cursorX) const nextCursor = new Selection(newCursorPosition, newCursorPosition) - editor.selection = nextCursor + editor.selections[0] = nextCursor } updateParenTrails(mode, editor, result.parenTrails) diff --git a/src/util.js b/src/util.js index 6bd7937..b278e1e 100644 --- a/src/util.js +++ b/src/util.js @@ -2,6 +2,18 @@ function isString (s) { return typeof s === 'string' } +// https://tinyurl.com/y7paps66 +function debounce (fn, interval) { + let timeout = 0 + return function () { + clearTimeout(timeout) + const args = arguments + timeout = setTimeout(function () { + fn.apply(null, args) + }, interval) + } +} + function atom (val) { let watchers = [] const notify = () => watchers.forEach((f) => f(val)) @@ -117,12 +129,13 @@ function isRunState (state) { } exports.atom = atom +exports.debounce = debounce exports.findEndRow = findEndRow exports.findStartRow = findStartRow exports.getTextFromRange = getTextFromRange exports.isParentExprLine = isParentExprLine -exports.isString = isString exports.isRunState = isRunState +exports.isString = isString exports.linesDiff = linesDiff exports.map = map exports.splitLines = splitLines From 53ffd38d0b9303c7f62f31ab2378362d12ab81e2 Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Mon, 20 Aug 2018 00:41:06 -0500 Subject: [PATCH 03/11] WIP - add test suite --- package.json | 4 +- src/extension.js | 100 +++++++++++++++++++++++++--------- src/parinfer.js | 17 ++---- src/util.js | 72 +++++++++++++++++++++++++ test/test.js | 137 +++++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 83 +++++++++++++++++++--------- 6 files changed, 349 insertions(+), 64 deletions(-) create mode 100644 test/test.js diff --git a/package.json b/package.json index 50d8136..a3b1150 100644 --- a/package.json +++ b/package.json @@ -75,13 +75,13 @@ ] }, "scripts": { - "test": "standard src/*.js" + "test": "standard src/*.js test/*.js && mocha" }, "dependencies": { "parinfer": "3.12.0" }, "devDependencies": { + "mocha": "5.2.0", "standard": "11.0.1" } - } diff --git a/src/extension.js b/src/extension.js index 1a4e7d7..1b3237f 100644 --- a/src/extension.js +++ b/src/extension.js @@ -29,14 +29,15 @@ const selectionChangeEvent = 'SELECTION_CHANGE' // TODO: need documentation for how the eventsQueue works let eventsQueue = [] +let ignoreNextEdit = false let prevCursorLine = null let prevCursorX = null let prevTxt = null -const logEventsQueue = false +const logEventsQueue = true function processEventsQueue () { - // do nothing if the queue is empty + // do nothing if the queue is empty (this should never happen) if (eventsQueue.length === 0) return const activeEditor = window.activeTextEditor @@ -45,13 +46,6 @@ function processEventsQueue () { // exit if we are not in Smart, Indent, or Paren mode if (!util.isRunState(editorMode)) return - if (logEventsQueue) { - if (eventsQueue[3]) console.log('3: ', eventsQueue[3]) - if (eventsQueue[2]) console.log('2: ', eventsQueue[2]) - if (eventsQueue[1]) console.log('1: ', eventsQueue[1]) - console.log('0: ', eventsQueue[0]) - } - // current text / previous text const currentTxt = eventsQueue[0].txt prevTxt = currentTxt @@ -62,32 +56,43 @@ function processEventsQueue () { cursorX: eventsQueue[0].cursorX } - // grab the document changes - if (eventsQueue[0].type === documentChangeEvent && eventsQueue[0].changes) { - options.changes = eventsQueue[0].changes - } else if (eventsQueue[0].type === selectionChangeEvent && - eventsQueue[1] && - eventsQueue[1].type === documentChangeEvent && - eventsQueue[1].changes) { - options.changes = eventsQueue[1].changes - } - // previous cursor information options.prevCursorLine = prevCursorLine options.prevCursorX = prevCursorX prevCursorLine = options.cursorLine prevCursorX = options.cursorX + // grab the document changes + let changes = [] + let i = eventsQueue.length - 1 + while (i >= 0) { + if (eventsQueue[i] && + eventsQueue[i].type === documentChangeEvent && + eventsQueue[i].changes) { + changes = changes.concat(eventsQueue[i].changes) + } + i = i - 1 + } + if (changes.length > 0) { + options.changes = changes + } + if (logEventsQueue) { + // console.log(JSON.stringify(eventsQueue, null, 2)) console.log('Parinfer options: ' + JSON.stringify(options)) console.log('~~~~~~~~~~~~~~~~ eventsQueue ~~~~~~~~~~~~~~~~') } + // clear out the event queue and ignore the next edit (since Parinfer will make it) + // FIXME: set this flag just before making the edit + // FIXME: we probably need a second flag for the selection change + // (assuming that Parinfer changes the cursor) eventsQueue.length = 0 + ignoreNextEdit = true parinfer2.applyParinfer(activeEditor, currentTxt, options) } -const debouncedProcessEventsQueue = debounce(processEventsQueue, 5) +const debouncedProcessEventsQueue = debounce(processEventsQueue, 1000) // ----------------------------------------------------------------------------- // Change Editor State @@ -124,6 +129,7 @@ editorStates.addWatch(onChangeEditorStates) function onChangeActiveEditor (editor) { // clear out the state when we switch the active editor eventsQueue.length = 0 + ignoreNextEdit = false prevCursorLine = null prevCursorX = null prevTxt = null @@ -143,8 +149,49 @@ function convertChangeObjects (oldTxt, changeEvt) { } } +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat +function repeatString (char, length) { + let newStr = '' + for (let i = 0; i < length; i++) { + newStr = newStr + 'x' + } + return newStr +} + +// convert TextDocumentChangeEvent to the format Parinfer expects +function convertChangeEvent (change) { + // const start = { + // line: change.range.start.line, + // char: change.range.start.character + // } + // const end = { + // line: change.range.end.line, + // char: change.range.end.character + // } + // return { + // rangeStart: start, + // rangeEnd: end, + // rangeOffset: change.rangeOffset, + // rangeLength: change.rangeLength, + // text: change.text + // } + + return { + lineNo: change.range.start.line, + newText: change.text, + oldText: repeatString('x', change.rangeLength), + x: change.range.start.character + } +} + // this function fires any time a document's content is changed function onChangeTextDocument (evt) { + // ignore edits that were made by Parinfer + if (ignoreNextEdit) { + ignoreNextEdit = false + return + } + // drop any events that do not contain document changes // NOTE: this is usually the first change to a document and anytime the user presses "save" if (evt.contentChanges && evt.contentChanges.length === 0) { @@ -154,6 +201,7 @@ function onChangeTextDocument (evt) { const activeEditor = window.activeTextEditor const theDocument = evt.document let parinferEvent = { + changes: evt.contentChanges.map(convertChangeEvent), cursorLine: activeEditor.selections[0].active.line, cursorX: activeEditor.selections[0].active.character, documentVersion: theDocument.version, @@ -161,11 +209,14 @@ function onChangeTextDocument (evt) { type: documentChangeEvent } + // console.log(JSON.stringify(parinferEvent.changes, null, 2)) + // console.log('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz') + // only create a "changes" property if there was prior text - if (prevTxt) { - const convertFn = convertChangeObjects.bind(null, prevTxt) - parinferEvent.changes = evt.contentChanges.map(convertFn) - } + // if (prevTxt) { + // const convertFn = convertChangeObjects.bind(null, prevTxt) + // parinferEvent.changes = evt.contentChanges.map(convertFn) + // } // put this event on the queue and schedule a processing eventsQueue.unshift(parinferEvent) @@ -173,6 +224,7 @@ function onChangeTextDocument (evt) { } // this function fires any time a cursor's position changes (ie: often) +// FIXME: make sure to ignore this event if it was caused by Parinfer function onChangeSelection (evt) { const editor = evt.textEditor const theDocument = editor.document diff --git a/src/parinfer.js b/src/parinfer.js index 0a19def..3d24675 100644 --- a/src/parinfer.js +++ b/src/parinfer.js @@ -1,24 +1,17 @@ const vscode = require('vscode') const Position = vscode.Position const Selection = vscode.Selection - const window = vscode.window const workspace = vscode.workspace - const parinfer = require('parinfer') - const parenTrailsModule = require('./parentrails') const updateParenTrails = parenTrailsModule.updateParenTrails - const editorModule = require('./editor') const editorStates = editorModule.editorStates - const util = require('./util') - const messages = require('./messages') const parenModeFailedMsg = messages.parenModeFailedMsg const parenModeChangedFileMsg = messages.parenModeChangedFileMsg - const config = require('./config') // ----------------------------------------------------------------------------- @@ -54,11 +47,11 @@ function applyParinfer2 (editor, inputText, opts, mode) { // FIXME: I think there are some cases where we can show an error here? if (!result.success) return - // if the text was unchanged, update the paren trails and exit - if (result.text === inputText) { - updateParenTrails(mode, editor, result.parenTrails) - return - } + // // if the text was unchanged, update the paren trails and exit + // if (result.text === inputText) { + // updateParenTrails(mode, editor, result.parenTrails) + // return + // } const undoOptions = { undoStopAfter: false, diff --git a/src/util.js b/src/util.js index b278e1e..31cebf6 100644 --- a/src/util.js +++ b/src/util.js @@ -110,6 +110,7 @@ function linesDiff (textA, textB) { }, initialCount) } +// FIXME: almost certainly this function has a bug - does multiline work? function getTextFromRange (txt, range, length) { if (length === 0) return '' @@ -122,12 +123,82 @@ function getTextFromRange (txt, range, length) { return line.substring(firstChar, firstChar + length) } +function replaceWithinString(orig, start, end, replace) { + return ( + orig.substring(0, start) + + replace + + orig.substring(end) + ); +} + +function joinChanges_OLD (change1, change2) { + let newChange = { + lineNo: change1.lineNo, + oldText: change1.oldText, + x: change1.x + } + + // const minX = Math.min(change1.x, change2.x) + // const maxX = Math.max(change1.x + change1.newText.length, change2.x + change2.newText.length) + // + // const line1 = [] + // const line2 = [] + + // change2 is either 1) an insert 2) a delete or 3) replace + const isInsertOrReplace = change2.newText.length >= change2.oldText.length + const isDelete = change2.newText.length < change2.oldText.length + + if (isInsertOrReplace) { + const startIdx = change2.x - change1.x + const changeLength = change2.newText.length + const endIdx = startIdx + changeLength + newChange.newText = replaceWithinString(change1.newText, startIdx, endIdx, change2.newText) + } else if (isDelete) { + //newChange. + } + + return newChange +} + +function joinChanges (change1, change2) { + const minX = Math.min(change1.x, change2.x) + const maxX = Math.max(change1.x + change1.oldText.length, change1.x + change1.newText.length, + change2.x + change2.oldText.length, change2.x + change2.newText.length) + const arrSize = maxX - minX + + + // const minX = Math.min(change1.x, change2.x) + // const maxX = Math.max(change1.x + change1.newText.length, change2.x + change2.newText.length) + // + // const line1 = [] + // const line2 = [] + + // change2 is either 1) an insert 2) a delete or 3) replace + const isInsertOrReplace = change2.newText.length >= change2.oldText.length + const isDelete = change2.newText.length < change2.oldText.length + + if (isInsertOrReplace) { + const startIdx = change2.x - change1.x + const changeLength = change2.newText.length + const endIdx = startIdx + changeLength + newChange.newText = replaceWithinString(change1.newText, startIdx, endIdx, change2.newText) + } else if (isDelete) { + + } + + return newChange +} + function isRunState (state) { return state === 'INDENT_MODE' || state === 'SMART_MODE' || state === 'PAREN_MODE' } +// ----------------------------------------------------------------------------- +// Exports +// ----------------------------------------------------------------------------- + exports.atom = atom exports.debounce = debounce exports.findEndRow = findEndRow @@ -136,6 +207,7 @@ exports.getTextFromRange = getTextFromRange exports.isParentExprLine = isParentExprLine exports.isRunState = isRunState exports.isString = isString +exports.joinChanges = joinChanges exports.linesDiff = linesDiff exports.map = map exports.splitLines = splitLines diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..7fab37a --- /dev/null +++ b/test/test.js @@ -0,0 +1,137 @@ +/* global describe, it */ + +const assert = require('assert') +const util = require('../src/util.js') + +// ----------------------------------------------------------------------------- +// Util +// ----------------------------------------------------------------------------- + +const change1a = { + lineNo: 1, + newText: 'a', + oldText: '', + x: 3 +} + +const change1b = { + lineNo: 1, + newText: 'bc', + oldText: '', + x: 4 +} + +const joinedChanged1 = { + lineNo: 1, + newText: 'abc', + oldText: '', + x: 3 +} + +const change2a = { + lineNo: 0, + newText: 'abc', + oldText: '', + x: 0 +} + +const change2b = { + lineNo: 0, + newText: 'x', + oldText: 'c', + x: 2 +} + +const joinedChanged2 = { + lineNo: 0, + newText: 'abx', + oldText: '', + x: 0 +} + +const change3a = { + lineNo: 0, + newText: 'abc', + oldText: '', + x: 1 +} + +const change3b = { + lineNo: 0, + newText: 'xyzxyz', + oldText: 'abc', + x: 1 +} + +const joinedChanged3 = { + lineNo: 0, + newText: 'xyzxyz', + oldText: '', + x: 1 +} + +const change4a = { + lineNo: 0, + newText: '', + oldText: 'c', + x: 2 +} + +const change4b = { + lineNo: 0, + newText: '', + oldText: 'b', + x: 1 +} + +const joinedChanged4 = { + lineNo: 0, + newText: '', + oldText: 'bc', + x: 1 +} + +const change5a = { + lineNo: 0, + newText: 'pear\npeach', + oldText: 'strawberry pineapple\nbanana raspberry', + x: 1 +} + +const change5b = { + lineNo: 1, + newText: 'orange', + oldText: 'peach', + x: 0 +} + +const joinedChanged5 = { + lineNo: 0, + newText: 'pear\norange', + oldText: 'strawberry pineapple\nbanana raspberry', + x: 1 +} + +function testUtil () { + it('joinChanges 1', function () { + assert.deepStrictEqual(util.joinChanges(change1a, change1b), joinedChanged1) + }) + + it('joinChanges 2', function () { + assert.deepStrictEqual(util.joinChanges(change2a, change2b), joinedChanged2) + }) + + it('joinChanges 3', function () { + assert.deepStrictEqual(util.joinChanges(change3a, change3b), joinedChanged3) + }) + + it('joinChanges 4', function () { + assert.deepStrictEqual(util.joinChanges(change4a, change4b), joinedChanged4) + }) + + it('joinChanges 5', function () { + assert.deepStrictEqual(util.joinChanges(change5a, change5b), joinedChanged5) + }) +} + +describe('util.js tests', testUtil) diff --git a/yarn.lock b/yarn.lock index 39b61d9..dfd425c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -97,6 +97,10 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -165,6 +169,10 @@ color-name@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689" +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -198,15 +206,15 @@ debug-log@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f" -debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" +debug@3.1.0, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: ms "2.0.0" -debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" +debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: ms "2.0.0" @@ -215,11 +223,10 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" define-properties@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" dependencies: - foreach "^2.0.5" - object-keys "^1.0.8" + object-keys "^1.0.12" deglob@^2.1.0: version "2.1.1" @@ -244,6 +251,10 @@ del@^2.0.2: pinkie-promise "^2.0.0" rimraf "^2.2.8" +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -281,7 +292,7 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -491,10 +502,6 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -511,7 +518,7 @@ get-stdin@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" -glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: +glob@7.1.2, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -541,6 +548,10 @@ graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -557,6 +568,10 @@ has@^1.0.1: dependencies: function-bind "^1.1.1" +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + hosted-git-info@^2.1.4: version "2.7.1" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" @@ -755,7 +770,7 @@ mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" -minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -769,12 +784,28 @@ minimist@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -mkdirp@^0.5.1: +mkdirp@0.5.1, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" +mocha@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -800,7 +831,7 @@ object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" -object-keys@^1.0.8: +object-keys@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" @@ -1044,8 +1075,8 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" shebang-command@^1.2.0: version "1.2.0" @@ -1149,16 +1180,16 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -supports-color@^5.3.0: +supports-color@5.4.0, supports-color@^5.3.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" dependencies: has-flag "^3.0.0" +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + table@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" From 78157e56911d9dfbcce07a57c5cef2c0d4503b93 Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Mon, 20 Aug 2018 19:19:28 -0500 Subject: [PATCH 04/11] WIP - test suite revamp --- package.json | 2 +- src/extension.js | 47 ++++----- src/util.js | 115 +++++++++++++--------- test/test.js | 251 +++++++++++++++++++++++------------------------ 4 files changed, 213 insertions(+), 202 deletions(-) diff --git a/package.json b/package.json index a3b1150..2b3c591 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ ] }, "scripts": { - "test": "standard src/*.js test/*.js && mocha" + "test": "standard && mocha" }, "dependencies": { "parinfer": "3.12.0" diff --git a/src/extension.js b/src/extension.js index 1b3237f..9193d7a 100644 --- a/src/extension.js +++ b/src/extension.js @@ -32,7 +32,6 @@ let eventsQueue = [] let ignoreNextEdit = false let prevCursorLine = null let prevCursorX = null -let prevTxt = null const logEventsQueue = true @@ -46,9 +45,7 @@ function processEventsQueue () { // exit if we are not in Smart, Indent, or Paren mode if (!util.isRunState(editorMode)) return - // current text / previous text const currentTxt = eventsQueue[0].txt - prevTxt = currentTxt // cursor options let options = { @@ -132,31 +129,31 @@ function onChangeActiveEditor (editor) { ignoreNextEdit = false prevCursorLine = null prevCursorX = null - prevTxt = null if (editor) { parinfer2.helloEditor(editor) } } -// convert VS Code change object to the format Parinfer expects -function convertChangeObjects (oldTxt, changeEvt) { - return { - lineNo: changeEvt.range.start.line, - newText: changeEvt.text, - oldText: util.getTextFromRange(oldTxt, changeEvt.range, changeEvt.rangeLength), - x: changeEvt.range.start.character - } -} +// FIXME: candidate for deletion +// // convert VS Code change object to the format Parinfer expects +// function convertChangeObjects (oldTxt, changeEvt) { +// return { +// lineNo: changeEvt.range.start.line, +// newText: changeEvt.text, +// oldText: util.getTextFromRange(oldTxt, changeEvt.range, changeEvt.rangeLength), +// x: changeEvt.range.start.character +// } +// } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat -function repeatString (char, length) { - let newStr = '' - for (let i = 0; i < length; i++) { - newStr = newStr + 'x' - } - return newStr -} +// function repeatString (char, length) { +// let newStr = '' +// for (let i = 0; i < length; i++) { +// newStr = newStr + 'x' +// } +// return newStr +// } // convert TextDocumentChangeEvent to the format Parinfer expects function convertChangeEvent (change) { @@ -177,9 +174,9 @@ function convertChangeEvent (change) { // } return { + changeLength: change.rangeLength, lineNo: change.range.start.line, - newText: change.text, - oldText: repeatString('x', change.rangeLength), + text: change.text, x: change.range.start.character } } @@ -212,12 +209,6 @@ function onChangeTextDocument (evt) { // console.log(JSON.stringify(parinferEvent.changes, null, 2)) // console.log('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz') - // only create a "changes" property if there was prior text - // if (prevTxt) { - // const convertFn = convertChangeObjects.bind(null, prevTxt) - // parinferEvent.changes = evt.contentChanges.map(convertFn) - // } - // put this event on the queue and schedule a processing eventsQueue.unshift(parinferEvent) debouncedProcessEventsQueue() diff --git a/src/util.js b/src/util.js index 31cebf6..dea87c3 100644 --- a/src/util.js +++ b/src/util.js @@ -123,70 +123,91 @@ function getTextFromRange (txt, range, length) { return line.substring(firstChar, firstChar + length) } -function replaceWithinString(orig, start, end, replace) { +function replaceWithinString (orig, start, end, replace) { return ( orig.substring(0, start) + replace + orig.substring(end) - ); + ) } -function joinChanges_OLD (change1, change2) { - let newChange = { - lineNo: change1.lineNo, - oldText: change1.oldText, - x: change1.x - } +const LINE_ENDING_REGEX = /\r?\n/ - // const minX = Math.min(change1.x, change2.x) - // const maxX = Math.max(change1.x + change1.newText.length, change2.x + change2.newText.length) - // - // const line1 = [] - // const line2 = [] - - // change2 is either 1) an insert 2) a delete or 3) replace - const isInsertOrReplace = change2.newText.length >= change2.oldText.length - const isDelete = change2.newText.length < change2.oldText.length - - if (isInsertOrReplace) { - const startIdx = change2.x - change1.x - const changeLength = change2.newText.length - const endIdx = startIdx + changeLength - newChange.newText = replaceWithinString(change1.newText, startIdx, endIdx, change2.newText) - } else if (isDelete) { - //newChange. +function changeType (change) { + if (change.text.length >= change.changeLength) { + return 'insert' + } else { + return 'delete' } - - return newChange } -function joinChanges (change1, change2) { - const minX = Math.min(change1.x, change2.x) - const maxX = Math.max(change1.x + change1.oldText.length, change1.x + change1.newText.length, - change2.x + change2.oldText.length, change2.x + change2.newText.length) - const arrSize = maxX - minX +function joinMultiLineChange (change1, change2) { + // TODO: write this + return null +} +function joinSingleLineDeleteDelete (change1, change2) { + // TODO: write me + return null +} - // const minX = Math.min(change1.x, change2.x) - // const maxX = Math.max(change1.x + change1.newText.length, change2.x + change2.newText.length) - // - // const line1 = [] - // const line2 = [] +function joinSingleLineDeleteInsert (change1, change2) { + // TODO: write me + return null +} - // change2 is either 1) an insert 2) a delete or 3) replace - const isInsertOrReplace = change2.newText.length >= change2.oldText.length - const isDelete = change2.newText.length < change2.oldText.length +function joinSingleLineInsertDelete (change1, change2) { + // TODO: write me + return null +} - if (isInsertOrReplace) { - const startIdx = change2.x - change1.x - const changeLength = change2.newText.length - const endIdx = startIdx + changeLength - newChange.newText = replaceWithinString(change1.newText, startIdx, endIdx, change2.newText) - } else if (isDelete) { +function joinSingleLineInsertInsert (change1, change2) { + const startIdx = change2.x - change1.x + const endIdx = startIdx + change2.changeLength + return { + changeLength: 0, + lineNo: change1.lineNo, + text: replaceWithinString(change1.text, startIdx, endIdx, change2.text), + x: change1.x + } +} +function joinSingleLineChange (change1, change2) { + const change1Type = changeType(change1) + const change2Type = changeType(change2) + + // four possible combinations: + // insert + insert + // insert + delete + // delete + insert + // delete + delete + if (change1Type === 'insert' && change2Type === 'insert') { + return joinSingleLineInsertInsert(change1, change2) + } else if (change1Type === 'insert' && change2Type === 'delete') { + return joinSingleLineInsertDelete(change1, change2) + } else if (change1Type === 'delete' && change2Type === 'insert') { + return joinSingleLineDeleteInsert(change1, change2) + } else if (change1Type === 'delete' && change2Type === 'delete') { + return joinSingleLineDeleteDelete(change1, change2) } +} - return newChange +// NOTES: +// - Shaun says that changes must be contiguous +// - We need a predicate function to check that two changes are contiguous +// - Get one-line changes working first, then tackle multi-line +function joinChanges (change1, change2) { + const change1Lines = change1.text.split(LINE_ENDING_REGEX) + const change2Lines = change2.text.split(LINE_ENDING_REGEX) + const isMultiLineChange = change1.lineNo !== change2.lineNo || + change1Lines.length > 1 || + change2Lines.length > 1 + + if (isMultiLineChange) { + return joinMultiLineChange(change1, change2) + } else { + return joinSingleLineChange(change1, change2) + } } function isRunState (state) { diff --git a/test/test.js b/test/test.js index 7fab37a..abae00c 100644 --- a/test/test.js +++ b/test/test.js @@ -4,134 +4,133 @@ const assert = require('assert') const util = require('../src/util.js') // ----------------------------------------------------------------------------- -// Util +// joinChanges // ----------------------------------------------------------------------------- -const change1a = { - lineNo: 1, - newText: 'a', - oldText: '', - x: 3 -} - -const change1b = { - lineNo: 1, - newText: 'bc', - oldText: '', - x: 4 -} - -const joinedChanged1 = { - lineNo: 1, - newText: 'abc', - oldText: '', - x: 3 -} - -const change2a = { - lineNo: 0, - newText: 'abc', - oldText: '', - x: 0 -} - -const change2b = { - lineNo: 0, - newText: 'x', - oldText: 'c', - x: 2 -} - -const joinedChanged2 = { - lineNo: 0, - newText: 'abx', - oldText: '', - x: 0 -} - -const change3a = { - lineNo: 0, - newText: 'abc', - oldText: '', - x: 1 -} - -const change3b = { - lineNo: 0, - newText: 'xyzxyz', - oldText: 'abc', - x: 1 -} - -const joinedChanged3 = { - lineNo: 0, - newText: 'xyzxyz', - oldText: '', - x: 1 -} - -const change4a = { - lineNo: 0, - newText: '', - oldText: 'c', - x: 2 -} - -const change4b = { - lineNo: 0, - newText: '', - oldText: 'b', - x: 1 -} - -const joinedChanged4 = { - lineNo: 0, - newText: '', - oldText: 'bc', - x: 1 -} - -const change5a = { - lineNo: 0, - newText: 'pear\npeach', - oldText: 'strawberry pineapple\nbanana raspberry', - x: 1 -} - -const change5b = { - lineNo: 1, - newText: 'orange', - oldText: 'peach', - x: 0 -} - -const joinedChanged5 = { - lineNo: 0, - newText: 'pear\norange', - oldText: 'strawberry pineapple\nbanana raspberry', - x: 1 -} - -function testUtil () { - it('joinChanges 1', function () { - assert.deepStrictEqual(util.joinChanges(change1a, change1b), joinedChanged1) - }) - - it('joinChanges 2', function () { - assert.deepStrictEqual(util.joinChanges(change2a, change2b), joinedChanged2) - }) - - it('joinChanges 3', function () { - assert.deepStrictEqual(util.joinChanges(change3a, change3b), joinedChanged3) - }) - - it('joinChanges 4', function () { - assert.deepStrictEqual(util.joinChanges(change4a, change4b), joinedChanged4) - }) - - it('joinChanges 5', function () { - assert.deepStrictEqual(util.joinChanges(change5a, change5b), joinedChanged5) +function isChangeObject (obj) { + return obj.hasOwnProperty('changeLength') && typeof obj.changeLength === 'number' && + obj.hasOwnProperty('lineNo') && typeof obj.lineNo === 'number' && + obj.hasOwnProperty('text') && typeof obj.text === 'string' && + obj.hasOwnProperty('x') && typeof obj.x === 'number' +} + +const changeExamples = [ + { + description: 'two basic inserts', + change1: { + changeLength: 0, + lineNo: 1, + text: 'a', + x: 3 + }, + change2: { + changeLength: 0, + lineNo: 1, + text: 'bc', + x: 4 + }, + result: { + changeLength: 0, + lineNo: 1, + text: 'abc', + x: 3 + } + }, + { + description: 'basic insert and a replace', + change1: { + changeLength: 0, + lineNo: 0, + text: 'abc', + x: 0 + }, + change2: { + changeLength: 1, + lineNo: 0, + text: 'z', + x: 2 + }, + result: { + changeLength: 0, + lineNo: 0, + text: 'abz', + x: 0 + } + }, + { + description: 'insert, then insert beyond first range', + change1: { + changeLength: 0, + lineNo: 0, + text: 'abc', + x: 1 + }, + change2: { + changeLength: 3, + lineNo: 0, + text: 'xyzxyz', + x: 1 + }, + result: { + changeLength: 0, + lineNo: 0, + text: 'xyzxyz', + x: 1 + } + }, + { + description: 'two simple deletes', + change1: { + changeLength: 1, + lineNo: 0, + text: '', + x: 2 + }, + change2: { + changeLength: 1, + lineNo: 0, + text: '', + x: 1 + }, + result: { + changeLength: 2, + lineNo: 0, + text: '', + x: 1 + } + } +] + +// const change5a = { +// lineNo: 0, +// text: 'pear\npeach', +// oldText: 'strawberry pineapple\nbanana raspberry', +// x: 1 +// } +// +// const change5b = { +// lineNo: 1, +// text: 'orange', +// oldText: 'peach', +// x: 0 +// } +// +// const joinedChanged5 = { +// lineNo: 0, +// text: 'pear\norange', +// oldText: 'strawberry pineapple\nbanana raspberry', +// x: 1 +// } + +function testJoinChanges () { + changeExamples.forEach(function (itm) { + it(itm.description, function () { + assert.ok(isChangeObject(itm.change1), '"change1" object is formatted wrong for test "' + itm.description + '"') + assert.ok(isChangeObject(itm.change2), '"change2" object is formatted wrong for test "' + itm.description + '"') + assert.ok(isChangeObject(itm.result), '"result" object is formatted wrong for test "' + itm.description + '"') + assert.deepStrictEqual(util.joinChanges(itm.change1, itm.change2), itm.result) + }) }) } -describe('util.js tests', testUtil) +describe('joinChanges', testJoinChanges) From 61eb5a7a3b647dfcd231491de628df60637b618d Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Mon, 20 Aug 2018 22:55:38 -0500 Subject: [PATCH 05/11] WIP - delete works --- src/util.js | 11 +++++++++-- test/test.js | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/util.js b/src/util.js index dea87c3..40786a0 100644 --- a/src/util.js +++ b/src/util.js @@ -147,8 +147,15 @@ function joinMultiLineChange (change1, change2) { } function joinSingleLineDeleteDelete (change1, change2) { - // TODO: write me - return null + const minX = Math.min(change1.x, change2.x) + const maxX = Math.max(change1.x + change1.changeLength, change2.x + change2.changeLength) + + return { + changeLength: maxX - minX, + lineNo: change1.lineNo, + text: '', + x: change2.x + } } function joinSingleLineDeleteInsert (change1, change2) { diff --git a/test/test.js b/test/test.js index abae00c..8830e77 100644 --- a/test/test.js +++ b/test/test.js @@ -98,6 +98,27 @@ const changeExamples = [ text: '', x: 1 } + }, + { + description: 'longer deletes', + change1: { + changeLength: 4, + lineNo: 2, + text: '', + x: 4 + }, + change2: { + changeLength: 3, + lineNo: 2, + text: '', + x: 1 + }, + result: { + changeLength: 7, + lineNo: 2, + text: '', + x: 1 + } } ] From 3718ee56c74d8001af018297bedc94e647431d4c Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Wed, 22 Aug 2018 21:50:05 -0500 Subject: [PATCH 06/11] saving state - this is maddening --- src/extension.js | 104 ++++++++++++++++++++--------------------------- src/parinfer.js | 83 +++++++++++++++++++++++-------------- src/state.js | 7 ++++ test/test.js | 2 + 4 files changed, 106 insertions(+), 90 deletions(-) create mode 100644 src/state.js diff --git a/src/extension.js b/src/extension.js index 9193d7a..b72dbe7 100644 --- a/src/extension.js +++ b/src/extension.js @@ -2,6 +2,7 @@ // Requires // ----------------------------------------------------------------------------- +const fs = require('fs') const vscode = require('vscode') const window = vscode.window const statusBar = require('./statusbar') @@ -10,11 +11,11 @@ const editorStates = editorModule.editorStates const parenTrailsModule = require('./parentrails') const clearParenTrailDecorators = parenTrailsModule.clearParenTrailDecorators const parinfer2 = require('./parinfer') -const util = require('./util') -const debounce = util.debounce const config = require('./config') const path = require('path') -const fs = require('fs') +const state = require('./state') +const util = require('./util') +const debounce = util.debounce // ----------------------------------------------------------------------------- // Constants @@ -29,11 +30,10 @@ const selectionChangeEvent = 'SELECTION_CHANGE' // TODO: need documentation for how the eventsQueue works let eventsQueue = [] -let ignoreNextEdit = false let prevCursorLine = null let prevCursorX = null -const logEventsQueue = true +const logEventsQueue = false function processEventsQueue () { // do nothing if the queue is empty (this should never happen) @@ -60,36 +60,41 @@ function processEventsQueue () { prevCursorX = options.cursorX // grab the document changes - let changes = [] + let changes = null let i = eventsQueue.length - 1 while (i >= 0) { if (eventsQueue[i] && eventsQueue[i].type === documentChangeEvent && eventsQueue[i].changes) { - changes = changes.concat(eventsQueue[i].changes) + if (!changes) { + changes = eventsQueue[i].changes + } else { + for (let j = 0; j < changes.length; j++) { + if (eventsQueue[i].changes[j]) { + changes[j] = util.joinChanges(changes[j], eventsQueue[i].changes[j]) + } + } + } } i = i - 1 } - if (changes.length > 0) { - options.changes = changes + if (changes) { + options.changes = changes.map(changeToParinferFormat) } if (logEventsQueue) { - // console.log(JSON.stringify(eventsQueue, null, 2)) + console.log(JSON.stringify(eventsQueue, null, 2)) console.log('Parinfer options: ' + JSON.stringify(options)) console.log('~~~~~~~~~~~~~~~~ eventsQueue ~~~~~~~~~~~~~~~~') } - // clear out the event queue and ignore the next edit (since Parinfer will make it) - // FIXME: set this flag just before making the edit - // FIXME: we probably need a second flag for the selection change - // (assuming that Parinfer changes the cursor) + // clear out the event queue eventsQueue.length = 0 - ignoreNextEdit = true parinfer2.applyParinfer(activeEditor, currentTxt, options) } -const debouncedProcessEventsQueue = debounce(processEventsQueue, 1000) +const processQueueDebounceIntervalMs = 20 +const debouncedProcessEventsQueue = debounce(processEventsQueue, processQueueDebounceIntervalMs) // ----------------------------------------------------------------------------- // Change Editor State @@ -126,7 +131,7 @@ editorStates.addWatch(onChangeEditorStates) function onChangeActiveEditor (editor) { // clear out the state when we switch the active editor eventsQueue.length = 0 - ignoreNextEdit = false + state.ignoreNextEdit = false prevCursorLine = null prevCursorX = null @@ -135,44 +140,18 @@ function onChangeActiveEditor (editor) { } } -// FIXME: candidate for deletion -// // convert VS Code change object to the format Parinfer expects -// function convertChangeObjects (oldTxt, changeEvt) { -// return { -// lineNo: changeEvt.range.start.line, -// newText: changeEvt.text, -// oldText: util.getTextFromRange(oldTxt, changeEvt.range, changeEvt.rangeLength), -// x: changeEvt.range.start.character -// } -// } - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat -// function repeatString (char, length) { -// let newStr = '' -// for (let i = 0; i < length; i++) { -// newStr = newStr + 'x' -// } -// return newStr -// } - -// convert TextDocumentChangeEvent to the format Parinfer expects -function convertChangeEvent (change) { - // const start = { - // line: change.range.start.line, - // char: change.range.start.character - // } - // const end = { - // line: change.range.end.line, - // char: change.range.end.character - // } - // return { - // rangeStart: start, - // rangeEnd: end, - // rangeOffset: change.rangeOffset, - // rangeLength: change.rangeLength, - // text: change.text - // } +// convert TextDocumentChangeEvent to the format parinfer expects +function changeToParinferFormat (change) { + return { + lineNo: change.lineNo, + newText: change.text, + oldText: 'x'.repeat(change.changeLength), + x: change.x + } +} +// convert TextDocumentChangeEvent to a different format +function convertChangeEvent (change) { return { changeLength: change.rangeLength, lineNo: change.range.start.line, @@ -184,8 +163,9 @@ function convertChangeEvent (change) { // this function fires any time a document's content is changed function onChangeTextDocument (evt) { // ignore edits that were made by Parinfer - if (ignoreNextEdit) { - ignoreNextEdit = false + if (state.ignoreNextEdit) { + console.log('ignored a document change event') + state.ignoreNextEdit = false return } @@ -206,17 +186,21 @@ function onChangeTextDocument (evt) { type: documentChangeEvent } - // console.log(JSON.stringify(parinferEvent.changes, null, 2)) - // console.log('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz') - // put this event on the queue and schedule a processing eventsQueue.unshift(parinferEvent) debouncedProcessEventsQueue() } // this function fires any time a cursor's position changes (ie: often) -// FIXME: make sure to ignore this event if it was caused by Parinfer function onChangeSelection (evt) { + // ignore selection changes that were made by Parinfer + if (state.ignoreNextSelectionChange) { + console.log('ignored a selection change event') + state.ignoreNextSelectionChange = false + return + } + + console.log('selection change event') const editor = evt.textEditor const theDocument = editor.document const selection = evt.selections[0] diff --git a/src/parinfer.js b/src/parinfer.js index 3d24675..0dfbb60 100644 --- a/src/parinfer.js +++ b/src/parinfer.js @@ -13,6 +13,7 @@ const messages = require('./messages') const parenModeFailedMsg = messages.parenModeFailedMsg const parenModeChangedFileMsg = messages.parenModeChangedFileMsg const config = require('./config') +const state = require('./state') // ----------------------------------------------------------------------------- // Parinfer Application @@ -21,6 +22,11 @@ const config = require('./config') const logParinferInput = false const logParinferOutput = false +const undoOptions = { + undoStopAfter: false, + undoStopBefore: false +} + function applyParinfer2 (editor, inputText, opts, mode) { if (!opts) { opts = {} @@ -47,38 +53,55 @@ function applyParinfer2 (editor, inputText, opts, mode) { // FIXME: I think there are some cases where we can show an error here? if (!result.success) return - // // if the text was unchanged, update the paren trails and exit - // if (result.text === inputText) { - // updateParenTrails(mode, editor, result.parenTrails) - // return - // } - - const undoOptions = { - undoStopAfter: false, - undoStopBefore: false - } - - const editPromise = editor.edit(function (editBuilder) { - editBuilder.replace(editorModule.getEditorRange(editor), result.text) - }, undoOptions) - - editPromise.then(function (editWasApplied) { - if (editWasApplied) { - // set the new cursor position - // NOTE: ignore the cursor from Parinfer if the user has multiple cursors - if (Number.isInteger(result.cursorLine) && - Number.isInteger(result.cursorX) && - editor.selections.length <= 1) { - const newCursorPosition = new Position(result.cursorLine, result.cursorX) - const nextCursor = new Selection(newCursorPosition, newCursorPosition) - editor.selections[0] = nextCursor + const hasTextChanged = result.text !== inputText + const currentCursorLine = editor.selections[0].start.line + const currentCursorX = editor.selections[0].start.character + const hasCursorChanged = !areCursorsEqual(currentCursorLine, currentCursorX, + result.cursorLine, result.cursorX) + const newCursorPosition = new Position(result.cursorLine, result.cursorX) + const nextCursor = new Selection(newCursorPosition, newCursorPosition) + + // might be helpful: + // https://github.com/Microsoft/vscode/issues/16389 + // https://github.com/Microsoft/vscode/issues/32058 + + // text and cursor unchanged: update the paren trails + if (!hasTextChanged && !hasCursorChanged) { + console.log('no text change; no cursor change') + updateParenTrails(mode, editor, result.parenTrails) + // text unchanged; cursor needs to be updated + } else if (!hasTextChanged && hasCursorChanged) { + console.log('no text change; cursor update') + editor.selection = nextCursor + updateParenTrails(mode, editor, result.parenTrails) + // text changed + } else if (hasTextChanged) { + console.log('we are about to change the text') + state.ignoreNextEdit = true + state.ignoreNextSelectionChange = true + const theWholeDocumentRange = editorModule.getEditorRange(editor) + const editPromise = editor.edit(function (editBuilder) { + // editBuilder.delete(theWholeDocumentRange) + // editBuilder.insert(new Position(0, 0), result.text) + editBuilder.replace(editorModule.getEditorRange(editor), result.text) + }, undoOptions) + + editPromise.then(function (editWasApplied) { + if (editWasApplied) { + editor.selection = nextCursor + updateParenTrails(mode, editor, result.parenTrails) + } else { + // TODO: should we do something here if the edit fails? } + }) + } +} - updateParenTrails(mode, editor, result.parenTrails) - } else { - // TODO: should we do something here if the edit fails? - } - }) +function areCursorsEqual (oldCursorLine, oldCursorX, newCursorLine, newCursorX) { + return Number.isInteger(newCursorLine) && + Number.isInteger(newCursorX) && + oldCursorLine === newCursorLine && + oldCursorX === newCursorX } function applyParinfer (editor, text, opts) { diff --git a/src/state.js b/src/state.js new file mode 100644 index 0000000..e1d2151 --- /dev/null +++ b/src/state.js @@ -0,0 +1,7 @@ +// some shared state among modules + +let ignoreNextEdit = false +let ignoreNextSelectionChange = false + +exports.ignoreNextEdit = ignoreNextEdit +exports.ignoreNextSelectionChange = ignoreNextSelectionChange diff --git a/test/test.js b/test/test.js index 8830e77..a83d65d 100644 --- a/test/test.js +++ b/test/test.js @@ -120,6 +120,8 @@ const changeExamples = [ x: 1 } } + // FIXME: need more test cases here + // In particular: need multi-line ] // const change5a = { From e6b66e77f0cff528dff5c6ca5a6d163a154a9586 Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Fri, 24 Aug 2018 20:23:30 -0500 Subject: [PATCH 07/11] saving state --- package.json | 1 + src/extension.js | 129 ++++++++++++++++++++++++++++------------------- src/parinfer.js | 35 ++++++------- src/state.js | 8 +-- src/util.js | 5 ++ test/test.js | 46 +++++++++++++++++ 6 files changed, 150 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index 2b3c591..63b1c9d 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "test": "standard && mocha" }, "dependencies": { + "diff": "3.5.0", "parinfer": "3.12.0" }, "devDependencies": { diff --git a/src/extension.js b/src/extension.js index b72dbe7..26238a4 100644 --- a/src/extension.js +++ b/src/extension.js @@ -2,6 +2,7 @@ // Requires // ----------------------------------------------------------------------------- +const diff = require('diff') const fs = require('fs') const vscode = require('vscode') const window = vscode.window @@ -15,6 +16,7 @@ const config = require('./config') const path = require('path') const state = require('./state') const util = require('./util') +const isString = util.isString const debounce = util.debounce // ----------------------------------------------------------------------------- @@ -32,6 +34,7 @@ const selectionChangeEvent = 'SELECTION_CHANGE' let eventsQueue = [] let prevCursorLine = null let prevCursorX = null +let prevTxt = null const logEventsQueue = false @@ -45,14 +48,22 @@ function processEventsQueue () { // exit if we are not in Smart, Indent, or Paren mode if (!util.isRunState(editorMode)) return - const currentTxt = eventsQueue[0].txt - - // cursor options + // create the Parinfer options object let options = { cursorLine: eventsQueue[0].cursorLine, cursorX: eventsQueue[0].cursorX } + // text + changes + const currentTxt = eventsQueue[0].txt + options.changes = null + // FIXME: I think we want to ignore the .changes object if the eventsQueue + // only contains selection changes? + if (isString(prevTxt) && currentTxt !== prevTxt) { + options.changes = calculateChanges(prevTxt, currentTxt) + } + prevTxt = currentTxt + // previous cursor information options.prevCursorLine = prevCursorLine options.prevCursorX = prevCursorX @@ -60,27 +71,27 @@ function processEventsQueue () { prevCursorX = options.cursorX // grab the document changes - let changes = null - let i = eventsQueue.length - 1 - while (i >= 0) { - if (eventsQueue[i] && - eventsQueue[i].type === documentChangeEvent && - eventsQueue[i].changes) { - if (!changes) { - changes = eventsQueue[i].changes - } else { - for (let j = 0; j < changes.length; j++) { - if (eventsQueue[i].changes[j]) { - changes[j] = util.joinChanges(changes[j], eventsQueue[i].changes[j]) - } - } - } - } - i = i - 1 - } - if (changes) { - options.changes = changes.map(changeToParinferFormat) - } + // let changes = null + // let i = eventsQueue.length - 1 + // while (i >= 0) { + // if (eventsQueue[i] && + // eventsQueue[i].type === documentChangeEvent && + // eventsQueue[i].changes) { + // if (!changes) { + // changes = eventsQueue[i].changes + // } else { + // for (let j = 0; j < changes.length; j++) { + // if (eventsQueue[i].changes[j]) { + // changes[j] = util.joinChanges(changes[j], eventsQueue[i].changes[j]) + // } + // } + // } + // } + // i = i - 1 + // } + // if (changes) { + // options.changes = changes.map(changeToParinferFormat) + // } if (logEventsQueue) { console.log(JSON.stringify(eventsQueue, null, 2)) @@ -93,9 +104,25 @@ function processEventsQueue () { parinfer2.applyParinfer(activeEditor, currentTxt, options) } -const processQueueDebounceIntervalMs = 20 +const processQueueDebounceIntervalMs = 500 const debouncedProcessEventsQueue = debounce(processEventsQueue, processQueueDebounceIntervalMs) +// const diffOptions = { +// ignoreWhitespace: false, +// newlineIsToken: true +// } + +function calculateChanges (oldTxt, newTxt) { + const theDiff = diff.diffChars(oldTxt, newTxt) + + console.log(oldTxt) + console.log(newTxt) + console.log(theDiff) + console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') + + return null +} + // ----------------------------------------------------------------------------- // Change Editor State // ----------------------------------------------------------------------------- @@ -131,41 +158,41 @@ editorStates.addWatch(onChangeEditorStates) function onChangeActiveEditor (editor) { // clear out the state when we switch the active editor eventsQueue.length = 0 - state.ignoreNextEdit = false + state.ignoreDocumentVersion = null + state.ignoreNextSelectionChange = false prevCursorLine = null prevCursorX = null + prevTxt = null if (editor) { parinfer2.helloEditor(editor) } } -// convert TextDocumentChangeEvent to the format parinfer expects -function changeToParinferFormat (change) { - return { - lineNo: change.lineNo, - newText: change.text, - oldText: 'x'.repeat(change.changeLength), - x: change.x - } -} - -// convert TextDocumentChangeEvent to a different format -function convertChangeEvent (change) { - return { - changeLength: change.rangeLength, - lineNo: change.range.start.line, - text: change.text, - x: change.range.start.character - } -} +// // convert TextDocumentChangeEvent to the format parinfer expects +// function changeToParinferFormat (change) { +// return { +// lineNo: change.lineNo, +// newText: change.text, +// oldText: 'x'.repeat(change.changeLength), +// x: change.x +// } +// } +// +// // convert TextDocumentChangeEvent to a different format +// function convertChangeEvent (change) { +// return { +// changeLength: change.rangeLength, +// lineNo: change.range.start.line, +// text: change.text, +// x: change.range.start.character +// } +// } // this function fires any time a document's content is changed function onChangeTextDocument (evt) { // ignore edits that were made by Parinfer - if (state.ignoreNextEdit) { - console.log('ignored a document change event') - state.ignoreNextEdit = false + if (state.ignoreDocumentVersion === evt.document.version) { return } @@ -178,7 +205,7 @@ function onChangeTextDocument (evt) { const activeEditor = window.activeTextEditor const theDocument = evt.document let parinferEvent = { - changes: evt.contentChanges.map(convertChangeEvent), + // changes: evt.contentChanges.map(convertChangeEvent), cursorLine: activeEditor.selections[0].active.line, cursorX: activeEditor.selections[0].active.character, documentVersion: theDocument.version, @@ -195,12 +222,12 @@ function onChangeTextDocument (evt) { function onChangeSelection (evt) { // ignore selection changes that were made by Parinfer if (state.ignoreNextSelectionChange) { - console.log('ignored a selection change event') + // console.log('ignored a selection change event') state.ignoreNextSelectionChange = false return } - console.log('selection change event') + // console.log('selection change event') const editor = evt.textEditor const theDocument = editor.document const selection = evt.selections[0] diff --git a/src/parinfer.js b/src/parinfer.js index 0dfbb60..3a09d19 100644 --- a/src/parinfer.js +++ b/src/parinfer.js @@ -56,39 +56,40 @@ function applyParinfer2 (editor, inputText, opts, mode) { const hasTextChanged = result.text !== inputText const currentCursorLine = editor.selections[0].start.line const currentCursorX = editor.selections[0].start.character - const hasCursorChanged = !areCursorsEqual(currentCursorLine, currentCursorX, - result.cursorLine, result.cursorX) - const newCursorPosition = new Position(result.cursorLine, result.cursorX) - const nextCursor = new Selection(newCursorPosition, newCursorPosition) - // might be helpful: - // https://github.com/Microsoft/vscode/issues/16389 - // https://github.com/Microsoft/vscode/issues/32058 + let hasCursorChanged = false + let nextCursor = false + if (result.hasOwnProperty('cursorLine') && result.hasOwnProperty('cursorX')) { + hasCursorChanged = !areCursorsEqual( + currentCursorLine, currentCursorX, result.cursorLine, result.cursorX + ) + const newCursorPosition = new Position(result.cursorLine, result.cursorX) + nextCursor = new Selection(newCursorPosition, newCursorPosition) + } - // text and cursor unchanged: update the paren trails + // text and cursor unchanged: just update the paren trails if (!hasTextChanged && !hasCursorChanged) { - console.log('no text change; no cursor change') updateParenTrails(mode, editor, result.parenTrails) - // text unchanged; cursor needs to be updated + // text unchanged, but cursor needs to be updated } else if (!hasTextChanged && hasCursorChanged) { - console.log('no text change; cursor update') editor.selection = nextCursor updateParenTrails(mode, editor, result.parenTrails) // text changed } else if (hasTextChanged) { - console.log('we are about to change the text') - state.ignoreNextEdit = true + state.ignoreDocumentVersion = editor.document.version + 1 state.ignoreNextSelectionChange = true - const theWholeDocumentRange = editorModule.getEditorRange(editor) const editPromise = editor.edit(function (editBuilder) { - // editBuilder.delete(theWholeDocumentRange) - // editBuilder.insert(new Position(0, 0), result.text) + // NOTE: should this be delete + insert instead? + // https://github.com/Microsoft/vscode/issues/32058 editBuilder.replace(editorModule.getEditorRange(editor), result.text) }, undoOptions) + // FYI - https://github.com/Microsoft/vscode/issues/16389 editPromise.then(function (editWasApplied) { if (editWasApplied) { - editor.selection = nextCursor + if (nextCursor) { + editor.selection = nextCursor + } updateParenTrails(mode, editor, result.parenTrails) } else { // TODO: should we do something here if the edit fails? diff --git a/src/state.js b/src/state.js index e1d2151..3a5efa6 100644 --- a/src/state.js +++ b/src/state.js @@ -1,7 +1,3 @@ // some shared state among modules - -let ignoreNextEdit = false -let ignoreNextSelectionChange = false - -exports.ignoreNextEdit = ignoreNextEdit -exports.ignoreNextSelectionChange = ignoreNextSelectionChange +exports.ignoreDocumentVersion = null +exports.ignoreNextSelectionChange = false diff --git a/src/util.js b/src/util.js index 40786a0..7c922e9 100644 --- a/src/util.js +++ b/src/util.js @@ -217,6 +217,10 @@ function joinChanges (change1, change2) { } } +function diffChangesToParinferChanges (changes) { + +} + function isRunState (state) { return state === 'INDENT_MODE' || state === 'SMART_MODE' || @@ -229,6 +233,7 @@ function isRunState (state) { exports.atom = atom exports.debounce = debounce +exports.diffChangesToParinferChanges = diffChangesToParinferChanges exports.findEndRow = findEndRow exports.findStartRow = findStartRow exports.getTextFromRange = getTextFromRange diff --git a/test/test.js b/test/test.js index a83d65d..12dc0e9 100644 --- a/test/test.js +++ b/test/test.js @@ -157,3 +157,49 @@ function testJoinChanges () { } describe('joinChanges', testJoinChanges) + +// ----------------------------------------------------------------------------- +// diff changes +// ----------------------------------------------------------------------------- + +const diffExamples = [ + { + description: 'basic insert', + diff: [ + { + count: 4, + value: '(foo' + }, + { + count: 3, + added: true, + value: 'bar' + }, + { + count: 10, + value: ' [1 2 3])' + } + ], + parinfer: [ + { + lineNo: 0, + newText: 'bar', + oldText: '', + x: 4 + } + ] + } + + // FIXME: need more test cases here + // In particular: need multi-line +] + +function testDiffChanges () { + diffExamples.forEach(function (itm) { + it(itm.description, function () { + assert.deepStrictEqual(util.diffChangesToParinferChanges(itm.diff), itm.parinfer) + }) + }) +} + +describe('diff changes', testDiffChanges) From f5a9ae9d371b56b8576d4bc362d34592fb1af367 Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Fri, 24 Aug 2018 22:27:50 -0500 Subject: [PATCH 08/11] saving state --- src/extension.js | 5 --- src/util.js | 38 ++++++++++++++++++++++- test/test.js | 80 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 116 insertions(+), 7 deletions(-) diff --git a/src/extension.js b/src/extension.js index 26238a4..5787b96 100644 --- a/src/extension.js +++ b/src/extension.js @@ -107,11 +107,6 @@ function processEventsQueue () { const processQueueDebounceIntervalMs = 500 const debouncedProcessEventsQueue = debounce(processEventsQueue, processQueueDebounceIntervalMs) -// const diffOptions = { -// ignoreWhitespace: false, -// newlineIsToken: true -// } - function calculateChanges (oldTxt, newTxt) { const theDiff = diff.diffChars(oldTxt, newTxt) diff --git a/src/util.js b/src/util.js index 7c922e9..e9d9488 100644 --- a/src/util.js +++ b/src/util.js @@ -217,8 +217,44 @@ function joinChanges (change1, change2) { } } -function diffChangesToParinferChanges (changes) { +function diffChangesToParinferChanges (diffChanges) { + let parinferChanges = [] + let currentLineNo = 0 + + for (let i = 0; i < diffChanges.length; i++) { + const linesArr = diffChanges[i].value.split(LINE_ENDING_REGEX) + const numLines = linesArr.length + const lastLine = linesArr[numLines - 1] + + currentLineNo = currentLineNo + numLines - 1 + diffChanges[i].endLineNo = currentLineNo + diffChanges[i].endX = lastLine.length + + let prevX = 0 + let prevLineNo = 0 + if (diffChanges[i - 1]) { + prevLineNo = diffChanges[i - 1].endLineNo + prevX = diffChanges[i - 1].endX + } + + if (diffChanges[i].added) { + parinferChanges.push({ + lineNo: prevLineNo, + newText: diffChanges[i].value, + oldText: '', + x: prevX + }) + } else if (diffChanges[i].removed) { + parinferChanges.push({ + lineNo: prevLineNo, + newText: '', + oldText: diffChanges[i].value, + x: prevX + }) + } + } + return parinferChanges } function isRunState (state) { diff --git a/test/test.js b/test/test.js index 12dc0e9..b8a91c0 100644 --- a/test/test.js +++ b/test/test.js @@ -188,10 +188,88 @@ const diffExamples = [ x: 4 } ] + }, + { + description: 'basic delete', + diff: [ + { + count: 1, + value: '(' + }, + { + count: 1, + removed: true, + value: 'f' + }, + { + count: 15, + value: 'oobar [1 2 3])' + } + ], + parinfer: [ + { + lineNo: 0, + newText: '', + oldText: 'f', + x: 1 + } + ] + }, + { + description: 'multi-line insert', + diff: [ + { + count: 10, + value: 'abc\ndef\ngh' + }, + { + count: 11, + added: true, + value: 'rst\nuvw\nxyz' + }, + { + count: 5, + value: 'i\njkl' + } + ], + parinfer: [ + { + lineNo: 2, + newText: 'rst\nuvw\nxyz', + oldText: '', + x: 2 + } + ] + }, + { + description: 'multi-line delete', + diff: [ + { + count: 9, + value: 'apple\nban' + }, + { + count: 9, + removed: true, + value: 'ana\nstraw' + }, + { + count: 6, + value: 'berry' + } + ], + parinfer: [ + { + lineNo: 1, + newText: '', + oldText: 'ana\nstraw', + x: 3 + } + ] } // FIXME: need more test cases here - // In particular: need multi-line + // In particular: need more multi-line and multi-edit examples ] function testDiffChanges () { From a42b99463e0dfcd160d864f314fc5ba98391f40f Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Sat, 25 Aug 2018 12:24:39 -0500 Subject: [PATCH 09/11] wip - saving state --- src/extension.js | 118 +++++++++++++++-------------------------------- src/parinfer.js | 55 ++++++++-------------- src/state.js | 3 +- 3 files changed, 56 insertions(+), 120 deletions(-) diff --git a/src/extension.js b/src/extension.js index 5787b96..651b76c 100644 --- a/src/extension.js +++ b/src/extension.js @@ -37,6 +37,7 @@ let prevCursorX = null let prevTxt = null const logEventsQueue = false +const logTextChange = false function processEventsQueue () { // do nothing if the queue is empty (this should never happen) @@ -56,13 +57,19 @@ function processEventsQueue () { // text + changes const currentTxt = eventsQueue[0].txt + const didTextChange = currentTxt !== prevTxt + + if (logTextChange && didTextChange) { + console.log(prevTxt) + console.log('+++') + console.log(currentTxt) + console.log('~~~~~~~~~~~~~~~~ prev / current text ~~~~~~~~~~~~~~~~') + } + options.changes = null - // FIXME: I think we want to ignore the .changes object if the eventsQueue - // only contains selection changes? - if (isString(prevTxt) && currentTxt !== prevTxt) { + if (isString(prevTxt) && didTextChange) { options.changes = calculateChanges(prevTxt, currentTxt) } - prevTxt = currentTxt // previous cursor information options.prevCursorLine = prevCursorLine @@ -70,29 +77,6 @@ function processEventsQueue () { prevCursorLine = options.cursorLine prevCursorX = options.cursorX - // grab the document changes - // let changes = null - // let i = eventsQueue.length - 1 - // while (i >= 0) { - // if (eventsQueue[i] && - // eventsQueue[i].type === documentChangeEvent && - // eventsQueue[i].changes) { - // if (!changes) { - // changes = eventsQueue[i].changes - // } else { - // for (let j = 0; j < changes.length; j++) { - // if (eventsQueue[i].changes[j]) { - // changes[j] = util.joinChanges(changes[j], eventsQueue[i].changes[j]) - // } - // } - // } - // } - // i = i - 1 - // } - // if (changes) { - // options.changes = changes.map(changeToParinferFormat) - // } - if (logEventsQueue) { console.log(JSON.stringify(eventsQueue, null, 2)) console.log('Parinfer options: ' + JSON.stringify(options)) @@ -104,18 +88,12 @@ function processEventsQueue () { parinfer2.applyParinfer(activeEditor, currentTxt, options) } -const processQueueDebounceIntervalMs = 500 +const processQueueDebounceIntervalMs = 20 const debouncedProcessEventsQueue = debounce(processEventsQueue, processQueueDebounceIntervalMs) function calculateChanges (oldTxt, newTxt) { const theDiff = diff.diffChars(oldTxt, newTxt) - - console.log(oldTxt) - console.log(newTxt) - console.log(theDiff) - console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') - - return null + return util.diffChangesToParinferChanges(theDiff) } // ----------------------------------------------------------------------------- @@ -134,7 +112,11 @@ function onChangeEditorStates (states) { statusBar.updateStatusBar(currentEditorState) if (util.isRunState(currentEditorState)) { const txt = activeEditor.document.getText() - parinfer2.applyParinfer(activeEditor, txt, {}) + const options = { + cursorLine: activeEditor.selection.active.line, + cursorX: activeEditor.selection.active.character + } + parinfer2.applyParinfer(activeEditor, txt, options) } else { clearParenTrailDecorators(activeEditor) } @@ -154,57 +136,37 @@ function onChangeActiveEditor (editor) { // clear out the state when we switch the active editor eventsQueue.length = 0 state.ignoreDocumentVersion = null - state.ignoreNextSelectionChange = false prevCursorLine = null prevCursorX = null - prevTxt = null + prevTxt = editor.document.getText() if (editor) { parinfer2.helloEditor(editor) } } -// // convert TextDocumentChangeEvent to the format parinfer expects -// function changeToParinferFormat (change) { -// return { -// lineNo: change.lineNo, -// newText: change.text, -// oldText: 'x'.repeat(change.changeLength), -// x: change.x -// } -// } -// -// // convert TextDocumentChangeEvent to a different format -// function convertChangeEvent (change) { -// return { -// changeLength: change.rangeLength, -// lineNo: change.range.start.line, -// text: change.text, -// x: change.range.start.character -// } -// } - // this function fires any time a document's content is changed function onChangeTextDocument (evt) { - // ignore edits that were made by Parinfer - if (state.ignoreDocumentVersion === evt.document.version) { - return - } - // drop any events that do not contain document changes // NOTE: this is usually the first change to a document and anytime the user presses "save" if (evt.contentChanges && evt.contentChanges.length === 0) { return } + const theText = evt.document.getText() + + // ignore edits that were made by Parinfer + if (state.ignoreDocumentVersion === evt.document.version) { + prevTxt = theText + return + } + const activeEditor = window.activeTextEditor - const theDocument = evt.document let parinferEvent = { - // changes: evt.contentChanges.map(convertChangeEvent), - cursorLine: activeEditor.selections[0].active.line, - cursorX: activeEditor.selections[0].active.character, - documentVersion: theDocument.version, - txt: theDocument.getText(), + cursorLine: activeEditor.selection.active.line, + cursorX: activeEditor.selection.active.character, + documentVersion: evt.document.version, + txt: theText, type: documentChangeEvent } @@ -215,22 +177,14 @@ function onChangeTextDocument (evt) { // this function fires any time a cursor's position changes (ie: often) function onChangeSelection (evt) { - // ignore selection changes that were made by Parinfer - if (state.ignoreNextSelectionChange) { - // console.log('ignored a selection change event') - state.ignoreNextSelectionChange = false - return - } + // TODO: ignore events that were not made by a mouse or keyboard? - // console.log('selection change event') const editor = evt.textEditor - const theDocument = editor.document - const selection = evt.selections[0] const parinferEvent = { - cursorLine: selection.active.line, - cursorX: selection.active.character, - documentVersion: theDocument.version, - txt: theDocument.getText(), + cursorLine: editor.selection.active.line, + cursorX: editor.selection.active.character, + documentVersion: editor.document.version, + txt: editor.document.getText(), type: selectionChangeEvent } diff --git a/src/parinfer.js b/src/parinfer.js index 3a09d19..eb8b95d 100644 --- a/src/parinfer.js +++ b/src/parinfer.js @@ -19,19 +19,15 @@ const state = require('./state') // Parinfer Application // ----------------------------------------------------------------------------- -const logParinferInput = false -const logParinferOutput = false - const undoOptions = { undoStopAfter: false, undoStopBefore: false } -function applyParinfer2 (editor, inputText, opts, mode) { - if (!opts) { - opts = {} - } +const logParinferInput = false +const logParinferOutput = false +function applyParinfer2 (editor, inputText, opts, mode) { if (logParinferInput) { console.log(inputText) console.log(opts) @@ -44,6 +40,9 @@ function applyParinfer2 (editor, inputText, opts, mode) { else if (mode === 'SMART_MODE') result = parinfer.smartMode(inputText, opts) else if (mode === 'PAREN_MODE') result = parinfer.parenMode(inputText, opts) + console.assert(Number.isInteger(result.cursorLine), 'Parinfer result.cursorLine is not an integer') + console.assert(Number.isInteger(result.cursorX), 'Parinfer result.cursorX is not an integer') + if (logParinferOutput) { console.log(result) console.log('~~~~~~~~~~~~~~~~ parinfer output ~~~~~~~~~~~~~~~~') @@ -54,30 +53,23 @@ function applyParinfer2 (editor, inputText, opts, mode) { if (!result.success) return const hasTextChanged = result.text !== inputText - const currentCursorLine = editor.selections[0].start.line - const currentCursorX = editor.selections[0].start.character - - let hasCursorChanged = false - let nextCursor = false - if (result.hasOwnProperty('cursorLine') && result.hasOwnProperty('cursorX')) { - hasCursorChanged = !areCursorsEqual( - currentCursorLine, currentCursorX, result.cursorLine, result.cursorX - ) - const newCursorPosition = new Position(result.cursorLine, result.cursorX) - nextCursor = new Selection(newCursorPosition, newCursorPosition) + + const isSelectionEmpty = editor.selection.isEmpty + const anchorPosition = editor.selection.anchor + const newCursorPosition = new Position(result.cursorLine, result.cursorX) + let newSelection = null + if (isSelectionEmpty) { + newSelection = new Selection(newCursorPosition, newCursorPosition) + } else { + newSelection = new Selection(anchorPosition, newCursorPosition) } - // text and cursor unchanged: just update the paren trails - if (!hasTextChanged && !hasCursorChanged) { - updateParenTrails(mode, editor, result.parenTrails) - // text unchanged, but cursor needs to be updated - } else if (!hasTextChanged && hasCursorChanged) { - editor.selection = nextCursor + // text unchanged: just update the paren trails + if (!hasTextChanged) { updateParenTrails(mode, editor, result.parenTrails) // text changed - } else if (hasTextChanged) { + } else { state.ignoreDocumentVersion = editor.document.version + 1 - state.ignoreNextSelectionChange = true const editPromise = editor.edit(function (editBuilder) { // NOTE: should this be delete + insert instead? // https://github.com/Microsoft/vscode/issues/32058 @@ -87,9 +79,7 @@ function applyParinfer2 (editor, inputText, opts, mode) { // FYI - https://github.com/Microsoft/vscode/issues/16389 editPromise.then(function (editWasApplied) { if (editWasApplied) { - if (nextCursor) { - editor.selection = nextCursor - } + editor.selection = newSelection updateParenTrails(mode, editor, result.parenTrails) } else { // TODO: should we do something here if the edit fails? @@ -98,13 +88,6 @@ function applyParinfer2 (editor, inputText, opts, mode) { } } -function areCursorsEqual (oldCursorLine, oldCursorX, newCursorLine, newCursorX) { - return Number.isInteger(newCursorLine) && - Number.isInteger(newCursorX) && - oldCursorLine === newCursorLine && - oldCursorX === newCursorX -} - function applyParinfer (editor, text, opts) { // defensive if (!editor) return diff --git a/src/state.js b/src/state.js index 3a5efa6..e632c78 100644 --- a/src/state.js +++ b/src/state.js @@ -1,3 +1,2 @@ -// some shared state among modules +// state that is shared between modules exports.ignoreDocumentVersion = null -exports.ignoreNextSelectionChange = false From 2a225555caaf97296d45537f33a57ca237de8e34 Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Sat, 25 Aug 2018 12:34:24 -0500 Subject: [PATCH 10/11] minor --- src/parinfer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parinfer.js b/src/parinfer.js index eb8b95d..212b00b 100644 --- a/src/parinfer.js +++ b/src/parinfer.js @@ -52,7 +52,7 @@ function applyParinfer2 (editor, inputText, opts, mode) { // FIXME: I think there are some cases where we can show an error here? if (!result.success) return - const hasTextChanged = result.text !== inputText + const didTextChange = result.text !== inputText const isSelectionEmpty = editor.selection.isEmpty const anchorPosition = editor.selection.anchor @@ -65,7 +65,7 @@ function applyParinfer2 (editor, inputText, opts, mode) { } // text unchanged: just update the paren trails - if (!hasTextChanged) { + if (!didTextChange) { updateParenTrails(mode, editor, result.parenTrails) // text changed } else { From 55e6ef2e503d033849a0d52ecd02768699dba817 Mon Sep 17 00:00:00 2001 From: Chris Oakman Date: Sat, 25 Aug 2018 13:11:51 -0500 Subject: [PATCH 11/11] correctly track prevText --- src/extension.js | 16 ++++++---------- src/parinfer.js | 2 ++ src/state.js | 1 + 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/extension.js b/src/extension.js index 651b76c..87b7c3e 100644 --- a/src/extension.js +++ b/src/extension.js @@ -34,7 +34,6 @@ const selectionChangeEvent = 'SELECTION_CHANGE' let eventsQueue = [] let prevCursorLine = null let prevCursorX = null -let prevTxt = null const logEventsQueue = false const logTextChange = false @@ -57,18 +56,18 @@ function processEventsQueue () { // text + changes const currentTxt = eventsQueue[0].txt - const didTextChange = currentTxt !== prevTxt + const didTextChange = currentTxt !== state.prevTxt if (logTextChange && didTextChange) { - console.log(prevTxt) + console.log(state.prevTxt) console.log('+++') console.log(currentTxt) console.log('~~~~~~~~~~~~~~~~ prev / current text ~~~~~~~~~~~~~~~~') } options.changes = null - if (isString(prevTxt) && didTextChange) { - options.changes = calculateChanges(prevTxt, currentTxt) + if (isString(state.prevTxt) && didTextChange) { + options.changes = calculateChanges(state.prevTxt, currentTxt) } // previous cursor information @@ -138,7 +137,7 @@ function onChangeActiveEditor (editor) { state.ignoreDocumentVersion = null prevCursorLine = null prevCursorX = null - prevTxt = editor.document.getText() + state.prevTxt = editor.document.getText() if (editor) { parinfer2.helloEditor(editor) @@ -153,11 +152,8 @@ function onChangeTextDocument (evt) { return } - const theText = evt.document.getText() - // ignore edits that were made by Parinfer if (state.ignoreDocumentVersion === evt.document.version) { - prevTxt = theText return } @@ -166,7 +162,7 @@ function onChangeTextDocument (evt) { cursorLine: activeEditor.selection.active.line, cursorX: activeEditor.selection.active.character, documentVersion: evt.document.version, - txt: theText, + txt: evt.document.getText(), type: documentChangeEvent } diff --git a/src/parinfer.js b/src/parinfer.js index 212b00b..b6a5242 100644 --- a/src/parinfer.js +++ b/src/parinfer.js @@ -66,6 +66,7 @@ function applyParinfer2 (editor, inputText, opts, mode) { // text unchanged: just update the paren trails if (!didTextChange) { + state.prevTxt = result.text updateParenTrails(mode, editor, result.parenTrails) // text changed } else { @@ -80,6 +81,7 @@ function applyParinfer2 (editor, inputText, opts, mode) { editPromise.then(function (editWasApplied) { if (editWasApplied) { editor.selection = newSelection + state.prevTxt = result.text updateParenTrails(mode, editor, result.parenTrails) } else { // TODO: should we do something here if the edit fails? diff --git a/src/state.js b/src/state.js index e632c78..6feb34f 100644 --- a/src/state.js +++ b/src/state.js @@ -1,2 +1,3 @@ // state that is shared between modules exports.ignoreDocumentVersion = null +exports.prevTxt = null