Skip to content

Commit

Permalink
Merge pull request #590 from BetterThanTomorrow/pez/paredit-close-str…
Browse files Browse the repository at this point in the history
…ing-fix

REPL window prompt string closing
  • Loading branch information
PEZ authored Mar 19, 2020
2 parents b6c2177 + 4f27cf4 commit 56770d9
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Changes to Calva.

## [Unreleased]
- [Fix: REPL Window Paredit does not close strings properly](https://github.com/BetterThanTomorrow/calva/issues/587)

## [2.0.85] - 2020-03-15
- Fix: Make lein-shadow project type use lein injections
Expand Down
2 changes: 1 addition & 1 deletion src/cursor-doc/clojure-lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ let inString = new LexicalGrammar()
// end a string
inString.terminal(/"/, (l, m) => ({ type: "close" }))
// still within a string
inString.terminal(/(\\.|[^\\"\t\r\n ])+/, (l, m) => ({ type: "str-inside" }))
inString.terminal(/(\\.|[^"\s])+/, (l, m) => ({ type: "str-inside" }))
// whitespace, excluding newlines
inString.terminal(/[\t ]+/, (l, m) => ({ type: "ws" }))
// newlines, we want each one as a token of its own
Expand Down
39 changes: 21 additions & 18 deletions src/cursor-doc/paredit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,23 +350,14 @@ export function open(doc: EditableDocument, open: string, close: string, start:
}

export function close(doc: EditableDocument, close: string, start: number = doc.selectionRight) {
let cursor = doc.getTokenCursor();
const cursor = doc.getTokenCursor(start);
cursor.forwardWhitespace(false);
if (cursor.getToken().raw == close) {
doc.selection = new ModelEditSelection(start + close.length);
} else {
doc.model.edit([
new ModelEdit('changeRange', [start, cursor.offsetStart, ""])
new ModelEdit('changeRange', [start, start, close])
], { selection: new ModelEditSelection(start + close.length) });
} else {
// one of two things are possible:
if (cursor.forwardList()) {
// we are in a matched list, just jump to the end of it.
doc.selection = new ModelEditSelection(cursor.offsetEnd);
} else {
while (cursor.forwardSexp()) { }
doc.model.edit([
new ModelEdit('changeRange', [cursor.offsetEnd, cursor.offsetEnd, close])
], { selection: new ModelEditSelection(cursor.offsetEnd + close.length) });
}
}
}

Expand Down Expand Up @@ -429,12 +420,24 @@ export function stringQuote(doc: EditableDocument, start: number = doc.selection
let cursor = doc.getTokenCursor(start);
if (cursor.withinString()) {
// inside a string, let's be clever
if (cursor.offsetEnd - 1 == start && cursor.getToken().type == "str" || cursor.getToken().type == "str-end") {
doc.selection = new ModelEditSelection(start + 1);
if (cursor.getToken().type == "close") {
if (doc.model.getText(0, start).endsWith('\\')) {
doc.model.edit([
new ModelEdit('changeRange', [start, start, '"'])
], { selection: new ModelEditSelection(start + 1) });
} else {
close(doc, '"', start);
}
} else {
doc.model.edit([
new ModelEdit('changeRange', [start, start, '"'])
], { selection: new ModelEditSelection(start + 1) });
if (doc.model.getText(0, start).endsWith('\\')) {
doc.model.edit([
new ModelEdit('changeRange', [start, start, '"'])
], { selection: new ModelEditSelection(start + 1) });
} else {
doc.model.edit([
new ModelEdit('changeRange', [start, start, '\\"'])
], { selection: new ModelEditSelection(start + 2) });
}
}
} else {
doc.model.edit([
Expand Down
7 changes: 6 additions & 1 deletion src/cursor-doc/token-cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,12 @@ export class LispTokenCursor extends TokenCursor {
* Indicates if the current token is inside a string
*/
withinString() {
return this.getToken().type == 'str-inside' || this.getPrevToken().type == 'str-inside';
const cursor = this.clone();
cursor.backwardList();
if (cursor.getPrevToken().type === 'open' && cursor.getPrevToken().raw === '"') {
return true;
};
return false;
}

/**
Expand Down
14 changes: 11 additions & 3 deletions src/extension-test/unit/cursor-doc/clojure-lexer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,16 @@ describe('Scanner', () => {
expect(tokens[4].type).equals('close');
expect(tokens[4].raw).equals('"');
});
it('tokenizes quoted quotes in strings', () => {
let tokens = scanner.processLine('"\\""');
expect(tokens[0].type).equals('open');
expect(tokens[0].raw).equals('"');
expect(tokens[1].type).equals('str-inside');
expect(tokens[1].raw).equals('\\"');
tokens = scanner.processLine('"foo\\"bar"');
expect(tokens[1].type).equals('str-inside');
expect(tokens[1].raw).equals('foo\\"bar');
});
});
describe('Reported issues', () => {
it('too long lines - #566', () => {
Expand Down Expand Up @@ -282,6 +292,4 @@ describe('Scanner', () => {
expect(tokens[0].raw).equals("#'foo");
});
});
});


});
23 changes: 23 additions & 0 deletions src/extension-test/unit/cursor-doc/paredit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,26 @@ describe('paredit', () => {
});



describe('edits', () => {
it('Closes list', () => {
const doc: mock.MockDocument = new mock.MockDocument(),
text = '(str "foo")',
caret = 10;
doc.insertString(text);
doc.selection = new ModelEditSelection(caret);
paredit.close(doc, ')');
expect(doc.model.getText(0, Infinity)).equal(text);
expect(doc.selection).deep.equal(new ModelEditSelection(caret + 1));
});
it('Closes quote at end of string', () => {
const doc: mock.MockDocument = new mock.MockDocument(),
text = '(str "foo")',
caret = 9;
doc.insertString(text);
doc.selection = new ModelEditSelection(caret);
paredit.stringQuote(doc);
expect(doc.model.getText(0, Infinity)).equal(text);
expect(doc.selection).deep.equal(new ModelEditSelection(caret + 1));
});
});
64 changes: 64 additions & 0 deletions src/extension-test/unit/cursor-doc/token-cursor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,38 @@ describe('Token Cursor', () => {
cursor.backwardList();
expect(cursor.offsetStart).equal(5);
});

it('backwardUpList: (a(b(c•#f•(#b •|[:f :b :z])•#z•1))) => (a(b(c•|#f•(#b •[:f :b :z])•#z•1)))', () => {
const cursor: LispTokenCursor = doc.getTokenCursor(15);
cursor.backwardUpList();
expect(cursor.offsetStart).equal(7);
});

// TODO: Figure out why adding these tests make other test break!
describe('Navigation in and around strings', () => {
it('backwardList moves to start of string', () => {
const doc = new mock.MockDocument();
doc.insertString('(str [] "", "foo" "f b b" " f b b " "\\"" \\")');
const cursor: LispTokenCursor = doc.getTokenCursor(21);
cursor.backwardList();
expect(cursor.offsetStart).equal(19);
});
it('forwardList moves to end of string', () => {
const doc = new mock.MockDocument();
doc.insertString('(str [] "", "foo" "f b b" " f b b " "\\"" \\")');
const cursor: LispTokenCursor = doc.getTokenCursor(21);
cursor.forwardList();
expect(cursor.offsetStart).equal(27);
});
it('backwardSexpr inside string moves past quoted characters', () => {
const doc = new mock.MockDocument();
doc.insertString('(str [] "foo \\" bar")');
const cursor: LispTokenCursor = doc.getTokenCursor(15);
cursor.backwardSexp();
expect(cursor.offsetStart).equal(13);
});
})

describe('Current Form', () => {
const docText = '(aaa (bbb (ccc •#foo•(#bar •#baz•[:a :b :c]•x•#(a b c))•#baz•yyy• z z z •foo• • bar)))'.replace(//g, '\n'),
doc: mock.MockDocument = new mock.MockDocument();
Expand Down Expand Up @@ -114,4 +140,42 @@ describe('Token Cursor', () => {
expect(cursor.rangeForCurrentForm(1)).deep.equal([0, 2]);
});
});
describe('Location State', () => {
it('Knows when inside string', () => {
const doc = new mock.MockDocument();
doc.insertString('(str [] "", "foo" "f b b" " f b b " "\\"" \\")');
const withinEmpty = doc.getTokenCursor(9);
expect(withinEmpty.withinString()).equal(true);
const adjacentOutsideLeft = doc.getTokenCursor(8);
expect(adjacentOutsideLeft.withinString()).equal(false);
const adjacentOutsideRight = doc.getTokenCursor(10);
expect(adjacentOutsideRight.withinString()).equal(false);
const noStringWS = doc.getTokenCursor(11);
expect(noStringWS.withinString()).equal(false);
const leftOfFirstWord = doc.getTokenCursor(13);
expect(leftOfFirstWord.withinString()).equal(true);
const rightOfLastWord = doc.getTokenCursor(16);
expect(rightOfLastWord.withinString()).equal(true);
const inWord = doc.getTokenCursor(14);
expect(inWord.withinString()).equal(true);
const spaceBetweenWords = doc.getTokenCursor(21);
expect(spaceBetweenWords.withinString()).equal(true);
const spaceBeforeFirstWord = doc.getTokenCursor(33);
expect(spaceBeforeFirstWord.withinString()).equal(true);
const spaceAfterLastWord = doc.getTokenCursor(41);
expect(spaceAfterLastWord.withinString()).equal(true);
const beforeQuotedStringQuote = doc.getTokenCursor(46);
expect(beforeQuotedStringQuote.withinString()).equal(true);
const inQuotedStringQuote = doc.getTokenCursor(47);
expect(inQuotedStringQuote.withinString()).equal(true);
const afterQuotedStringQuote = doc.getTokenCursor(48);
expect(afterQuotedStringQuote.withinString()).equal(true);
const beforeLiteralQuote = doc.getTokenCursor(50);
expect(beforeLiteralQuote.withinString()).equal(false);
const inLiteralQuote = doc.getTokenCursor(51);
expect(inLiteralQuote.withinString()).equal(false);
const afterLiteralQuote = doc.getTokenCursor(52);
expect(afterLiteralQuote.withinString()).equal(false);
});
});
});

0 comments on commit 56770d9

Please sign in to comment.