Add autocomplete support for quoted names, and remove single quotes syntax [#34]

This commit is contained in:
David Evans 2018-01-20 12:10:41 +00:00
parent 394dcb0e42
commit ece615e2a0
9 changed files with 202 additions and 103 deletions

View File

@ -22,7 +22,7 @@ Goblin -> Bowie: What babe?
Bowie -> Goblin: The babe with the power
Goblin -> Bowie: What power?
note right of Bowie, Goblin: Most people get muddled here!
Bowie -> Goblin: 'The power of voodoo'
Bowie -> Goblin: "The power of voodoo"
Goblin -> Bowie: "Who-do?"
Bowie -> Goblin: You do!
Goblin -> Bowie: Do what?
@ -85,7 +85,7 @@ note over Foo, Bar: "Foo and Bar
on multiple lines"
note between Foo, Bar: Link
text right: 'Comments\nOver here!'
text right: "Comments\nOver here!"
state over Foo: Foo is ponderous
```
@ -133,19 +133,19 @@ A <- ]: Profit!
<img src="screenshots/MultilineText.png" alt="Multiline Text preview" width="200" align="right" />
```
title 'My Multiline
Title'
title "My Multiline
Title"
note over Foo: 'Also possible\nwith escapes'
note over Foo: "Also possible\nwith escapes"
Foo -> Bar: 'Lines of text\non this arrow'
Foo -> Bar: "Lines of text\non this arrow"
if 'Even multiline\ninside conditions like this'
Foo -> 'Multiline\nagent'
if "Even multiline\ninside conditions like this"
Foo -> "Multiline\nagent"
end
state over Foo: 'Newlines here,
too!'
state over Foo: "Newlines here,
too!"
```
### Themes
@ -206,13 +206,13 @@ A <- B: than writing the whole name
<img src="screenshots/Markdown.png" alt="Markdown preview" width="200" align="right" />
```
define 'Name with
**bold** and _italic_' as A
define 'Also `code`
and ~strikeout~' as B
define "Name with
**bold** and _italic_" as A
define "Also `code`
and ~strikeout~" as B
A -> B: '_**basic markdown
is supported!**_'
A -> B: "_**basic markdown
is supported!**_"
```
### Alternative Agent Ordering
@ -270,17 +270,16 @@ Comments begin with a `#` and end at the next newline:
Meta data can be provided with particular keywords:
```
title 'My title here'
title "My title here"
```
Quoting strings is usually optional, for example these are the same:
```
title 'My title here'
title "My title here"
title My title here
title "My title" here
title "My" 'title' "here"
title "My" "title" "here"
```
Each non-metadata line represents a step in the sequence, in order.
@ -293,7 +292,7 @@ Foo Bar -> Zig Zag: Do a thing
# With quotes, this is the same as:
'Foo Bar' -> 'Zig Zag': 'Do a thing'
"Foo Bar" -> "Zig Zag": "Do a thing"
```
Blocks surround steps, and can nest:

View File

@ -606,6 +606,15 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
const CM_ERROR = {type: 'error line-error', then: {'': 0}};
function suggestionsEqual(a, b) {
return (
(a.v === b.v) &&
(a.prefix === b.prefix) &&
(a.suffix === b.suffix) &&
(a.q === b.q)
);
}
const makeCommands = ((() => {
// The order of commands inside "then" blocks directly influences the
// order they are displayed to the user in autocomplete menus.
@ -882,9 +891,9 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
function cmCappedToken(token, current) {
if(Object.keys(current.then).length > 0) {
return token + ' ';
return {v: token, suffix: ' ', q: false};
} else {
return token + '\n';
return {v: token, suffix: '\n', q: false};
}
}
@ -907,9 +916,9 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
} else if(current.suggest === true) {
return [cmCappedToken(token, current)];
} else if(Array.isArray(current.suggest)) {
return current.suggest;
return current.suggest.map((v) => ({v, q: false}));
} else if(current.suggest) {
return [current.suggest];
return [{v: current.suggest, q: false}];
} else {
return null;
}
@ -925,7 +934,8 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}
array.mergeSets(
comp,
cmGetSuggestions(state, token, current, next)
cmGetSuggestions(state, token, current, next),
suggestionsEqual
);
});
return comp;
@ -939,7 +949,8 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}
array.mergeSets(
state['known' + locals.type],
[locals.value + ' ']
[{v: locals.value, suffix: ' ', q: true}],
suggestionsEqual
);
locals.type = '';
locals.value = '';
@ -1153,14 +1164,6 @@ define('sequence/Tokeniser',['./CodeMirrorMode'], (CMMode) => {
escapeWith: unescape,
baseToken: {q: true},
},
{
start: /'/y,
end: /'/y,
escape: /\\(.)/y,
escapeWith:
unescape,
baseToken: {q: true},
},
{start: /(?=[^ \t\r\n:+\-~*!<>,])/y, end: /(?=[ \t\r\n:+\-~*!<>,])|$/y},
{
start: /(?=[\-~<])/y,
@ -5643,6 +5646,17 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
const TRIMMER = /^([ \t]*)(.*)$/;
const SQUASH_START = /^[ \t\r\n:,]/;
const SQUASH_END = /[ \t\r\n]$/;
const REQUIRED_QUOTED = /[\r\n:,"]/;
const QUOTE_ESCAPE = /["\\]/g;
function suggestionsEqual(a, b) {
return (
(a.v === b.v) &&
(a.prefix === b.prefix) &&
(a.suffix === b.suffix) &&
(a.q === b.q)
);
}
function makeRanges(cm, line, chFrom, chTo) {
const ln = cm.getLine(line);
@ -5661,13 +5675,27 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
return ranges;
}
function makeHintItem(text, ranges) {
function wrapQuote(entry, quote) {
if(!quote && entry.q && REQUIRED_QUOTED.test(entry.v)) {
quote = '"';
}
let inner = entry.v;
if(quote) {
inner = quote + inner.replace(QUOTE_ESCAPE, '\\$&') + quote;
}
return (entry.prefix || '') + inner + (entry.suffix || '');
}
function makeHintItem(entry, ranges, quote) {
const quoted = wrapQuote(entry, quote);
return {
text: text,
displayText: (text === '\n') ? '<END>' : text.trim(),
className: (text === '\n') ? 'pick-virtual' : null,
from: SQUASH_START.test(text) ? ranges.squashFrom : ranges.wordFrom,
to: SQUASH_END.test(text) ? ranges.squashTo : ranges.wordTo,
text: quoted,
displayText: (quoted === '\n') ? '<END>' : quoted.trim(),
className: (quoted === '\n') ? 'pick-virtual' : null,
from: SQUASH_START.test(quoted) ?
ranges.squashFrom : ranges.wordFrom,
to: SQUASH_END.test(quoted) ?
ranges.squashTo : ranges.wordTo,
};
}
@ -5676,14 +5704,14 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
if(!identified) {
return [];
}
return identified.map((item) => (prefix + item + suffix));
return identified.map((item) => ({v: item, prefix, suffix, q: true}));
}
function populateGlobals(suggestions, globals = {}) {
for(let i = 0; i < suggestions.length;) {
if(typeof suggestions[i] === 'object') {
if(suggestions[i].global) {
const identified = getGlobals(suggestions[i], globals);
array.mergeSets(suggestions, identified);
array.mergeSets(suggestions, identified, suggestionsEqual);
suggestions.splice(i, 1);
} else {
++ i;
@ -5691,16 +5719,29 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
}
}
function getHints(cm, options) {
const cur = cm.getCursor();
const token = cm.getTokenAt(cur);
function getPartial(cur, token) {
let partial = token.string;
if(token.end > cur.ch) {
partial = partial.substr(0, cur.ch - token.start);
}
const parts = TRIMMER.exec(partial);
partial = parts[2];
const from = token.start + parts[1].length;
let quote = '';
if(partial[0] === '"') {
quote = partial[0];
partial = partial.substr(1);
}
return {
partial,
quote,
from: token.start + parts[1].length,
};
}
function getHints(cm, options) {
const cur = cm.getCursor();
const token = cm.getTokenAt(cur);
const {partial, from, quote} = getPartial(cur, token);
const continuation = (cur.ch > 0 && token.state.line.length > 0);
let comp = (continuation ?
@ -5716,18 +5757,22 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => {
const ranges = makeRanges(cm, cur.line, from, token.end);
let selfValid = false;
const list = (comp
.filter((opt) => opt.startsWith(partial))
.map((opt) => {
if(opt === partial + ' ' && !options.completeSingle) {
.filter(({v, q}) => (q || !quote) && v.startsWith(partial))
.map((o) => {
if(o.v === partial + ' ' && !options.completeSingle) {
selfValid = true;
return null;
}
return makeHintItem(opt, ranges);
return makeHintItem(o, ranges, quote);
})
.filter((opt) => (opt !== null))
);
if(selfValid && list.length > 0) {
list.unshift(makeHintItem(partial + ' ', ranges));
list.unshift(makeHintItem(
{v: partial, suffix: ' ', q: false},
ranges,
quote
));
}
return {

File diff suppressed because one or more lines are too long

View File

@ -196,7 +196,7 @@ note over Foo, Bar: "Foo and Bar
on multiple lines"
note between Foo, Bar: Link
text right: 'Comments\nOver here!'
text right: "Comments\nOver here!"
state over Foo: Foo is ponderous
</pre>
@ -235,19 +235,19 @@ A <- ]: Profit!
<h3 id="MultilineText">Multiline Text</h3>
<pre class="example" data-lang="sequence">
title 'My Multiline
Title'
title "My Multiline
Title"
note over Foo: 'Also possible\nwith escapes'
note over Foo: "Also possible\nwith escapes"
Foo -> Bar: 'Lines of text\non this arrow'
Foo -> Bar: "Lines of text\non this arrow"
if 'Even multiline\ninside conditions like this'
Foo -> 'Multiline\nagent'
if "Even multiline\ninside conditions like this"
Foo -> "Multiline\nagent"
end
state over Foo: 'Newlines here,
too!'
state over Foo: "Newlines here,
too!"
</pre>
<h3 id="Themes">Themes</h3>
@ -305,13 +305,13 @@ A <- B: than writing the whole name
<h3 id="Markdown">Markdown</h3>
<pre class="example" data-lang="sequence">
define 'Name with
**bold** and _italic_' as A
define 'Also `code`
and ~strikeout~' as B
define "Name with
**bold** and _italic_" as A
define "Also `code`
and ~strikeout~" as B
A -> B: '_**basic markdown
is supported!**_'
A -> B: "_**basic markdown
is supported!**_"
</pre>
<h3 id="AlternativeAgentOrdering">Alternative Agent Ordering</h3>

View File

@ -21,7 +21,7 @@
'Bowie -> Goblin: The babe with the power\n' +
'Goblin -> Bowie: What power?\n' +
'note right of Bowie, Goblin: Most people get muddled here!\n' +
'Bowie -> Goblin: \'The power of voodoo\'\n' +
'Bowie -> Goblin: "The power of voodoo"\n' +
'Goblin -> Bowie: "Who-do?"\n' +
'Bowie -> Goblin: You do!\n' +
'Goblin -> Bowie: Do what?\n' +

View File

@ -4,6 +4,17 @@ define(['core/ArrayUtilities'], (array) => {
const TRIMMER = /^([ \t]*)(.*)$/;
const SQUASH_START = /^[ \t\r\n:,]/;
const SQUASH_END = /[ \t\r\n]$/;
const REQUIRED_QUOTED = /[\r\n:,"]/;
const QUOTE_ESCAPE = /["\\]/g;
function suggestionsEqual(a, b) {
return (
(a.v === b.v) &&
(a.prefix === b.prefix) &&
(a.suffix === b.suffix) &&
(a.q === b.q)
);
}
function makeRanges(cm, line, chFrom, chTo) {
const ln = cm.getLine(line);
@ -22,13 +33,27 @@ define(['core/ArrayUtilities'], (array) => {
return ranges;
}
function makeHintItem(text, ranges) {
function wrapQuote(entry, quote) {
if(!quote && entry.q && REQUIRED_QUOTED.test(entry.v)) {
quote = '"';
}
let inner = entry.v;
if(quote) {
inner = quote + inner.replace(QUOTE_ESCAPE, '\\$&') + quote;
}
return (entry.prefix || '') + inner + (entry.suffix || '');
}
function makeHintItem(entry, ranges, quote) {
const quoted = wrapQuote(entry, quote);
return {
text: text,
displayText: (text === '\n') ? '<END>' : text.trim(),
className: (text === '\n') ? 'pick-virtual' : null,
from: SQUASH_START.test(text) ? ranges.squashFrom : ranges.wordFrom,
to: SQUASH_END.test(text) ? ranges.squashTo : ranges.wordTo,
text: quoted,
displayText: (quoted === '\n') ? '<END>' : quoted.trim(),
className: (quoted === '\n') ? 'pick-virtual' : null,
from: SQUASH_START.test(quoted) ?
ranges.squashFrom : ranges.wordFrom,
to: SQUASH_END.test(quoted) ?
ranges.squashTo : ranges.wordTo,
};
}
@ -37,14 +62,14 @@ define(['core/ArrayUtilities'], (array) => {
if(!identified) {
return [];
}
return identified.map((item) => (prefix + item + suffix));
return identified.map((item) => ({v: item, prefix, suffix, q: true}));
}
function populateGlobals(suggestions, globals = {}) {
for(let i = 0; i < suggestions.length;) {
if(typeof suggestions[i] === 'object') {
if(suggestions[i].global) {
const identified = getGlobals(suggestions[i], globals);
array.mergeSets(suggestions, identified);
array.mergeSets(suggestions, identified, suggestionsEqual);
suggestions.splice(i, 1);
} else {
++ i;
@ -52,16 +77,29 @@ define(['core/ArrayUtilities'], (array) => {
}
}
function getHints(cm, options) {
const cur = cm.getCursor();
const token = cm.getTokenAt(cur);
function getPartial(cur, token) {
let partial = token.string;
if(token.end > cur.ch) {
partial = partial.substr(0, cur.ch - token.start);
}
const parts = TRIMMER.exec(partial);
partial = parts[2];
const from = token.start + parts[1].length;
let quote = '';
if(partial[0] === '"') {
quote = partial[0];
partial = partial.substr(1);
}
return {
partial,
quote,
from: token.start + parts[1].length,
};
}
function getHints(cm, options) {
const cur = cm.getCursor();
const token = cm.getTokenAt(cur);
const {partial, from, quote} = getPartial(cur, token);
const continuation = (cur.ch > 0 && token.state.line.length > 0);
let comp = (continuation ?
@ -77,18 +115,22 @@ define(['core/ArrayUtilities'], (array) => {
const ranges = makeRanges(cm, cur.line, from, token.end);
let selfValid = false;
const list = (comp
.filter((opt) => opt.startsWith(partial))
.map((opt) => {
if(opt === partial + ' ' && !options.completeSingle) {
.filter(({v, q}) => (q || !quote) && v.startsWith(partial))
.map((o) => {
if(o.v === partial + ' ' && !options.completeSingle) {
selfValid = true;
return null;
}
return makeHintItem(opt, ranges);
return makeHintItem(o, ranges, quote);
})
.filter((opt) => (opt !== null))
);
if(selfValid && list.length > 0) {
list.unshift(makeHintItem(partial + ' ', ranges));
list.unshift(makeHintItem(
{v: partial, suffix: ' ', q: false},
ranges,
quote
));
}
return {

View File

@ -3,6 +3,15 @@ define(['core/ArrayUtilities'], (array) => {
const CM_ERROR = {type: 'error line-error', then: {'': 0}};
function suggestionsEqual(a, b) {
return (
(a.v === b.v) &&
(a.prefix === b.prefix) &&
(a.suffix === b.suffix) &&
(a.q === b.q)
);
}
const makeCommands = ((() => {
// The order of commands inside "then" blocks directly influences the
// order they are displayed to the user in autocomplete menus.
@ -279,9 +288,9 @@ define(['core/ArrayUtilities'], (array) => {
function cmCappedToken(token, current) {
if(Object.keys(current.then).length > 0) {
return token + ' ';
return {v: token, suffix: ' ', q: false};
} else {
return token + '\n';
return {v: token, suffix: '\n', q: false};
}
}
@ -304,9 +313,9 @@ define(['core/ArrayUtilities'], (array) => {
} else if(current.suggest === true) {
return [cmCappedToken(token, current)];
} else if(Array.isArray(current.suggest)) {
return current.suggest;
return current.suggest.map((v) => ({v, q: false}));
} else if(current.suggest) {
return [current.suggest];
return [{v: current.suggest, q: false}];
} else {
return null;
}
@ -322,7 +331,8 @@ define(['core/ArrayUtilities'], (array) => {
}
array.mergeSets(
comp,
cmGetSuggestions(state, token, current, next)
cmGetSuggestions(state, token, current, next),
suggestionsEqual
);
});
return comp;
@ -336,7 +346,8 @@ define(['core/ArrayUtilities'], (array) => {
}
array.mergeSets(
state['known' + locals.type],
[locals.value + ' ']
[{v: locals.value, suffix: ' ', q: true}],
suggestionsEqual
);
locals.type = '';
locals.value = '';

View File

@ -23,14 +23,6 @@ define(['./CodeMirrorMode'], (CMMode) => {
escapeWith: unescape,
baseToken: {q: true},
},
{
start: /'/y,
end: /'/y,
escape: /\\(.)/y,
escapeWith:
unescape,
baseToken: {q: true},
},
{start: /(?=[^ \t\r\n:+\-~*!<>,])/y, end: /(?=[ \t\r\n:+\-~*!<>,])|$/y},
{
start: /(?=[\-~<])/y,

View File

@ -102,7 +102,7 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
});
it('parses quoted strings as single tokens', () => {
const input = 'foo "zig zag" \'abc def\'';
const input = 'foo "zig zag" "abc def"';
const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([
token({s: '', v: 'foo', q: false}),
@ -111,6 +111,16 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
]);
});
it('does not consider single quotes as quotes', () => {
const input = 'foo \'zig zag\'';
const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([
token({s: '', v: 'foo', q: false}),
token({s: ' ', v: '\'zig', q: false}),
token({s: ' ', v: 'zag\'', q: false}),
]);
});
it('stores character positions around quoted strings', () => {
const input = '"foo bar"';
const tokens = tokeniser.tokenise(input);
@ -130,7 +140,7 @@ defineDescribe('Sequence Tokeniser', ['./Tokeniser'], (Tokeniser) => {
});
it('ignores quotes within comments', () => {
const input = 'foo # bar "\'baz\nzig';
const input = 'foo # bar "baz\nzig';
const tokens = tokeniser.tokenise(input);
expect(tokens).toEqual([
token({s: '', v: 'foo'}),