|
1 | | -import extractAbbreviation from '@emmetio/extract-abbreviation'; |
2 | 1 | import { expand } from '@emmetio/expand-abbreviation'; |
3 | 2 |
|
4 | | -const field = () => ''; |
5 | | - |
6 | | -const expandAbbreviation = (source, language) => |
7 | | - expand(source.abbreviation, { |
8 | | - field, |
9 | | - syntax: language, |
10 | | - addons: { |
11 | | - jsx: true, |
12 | | - }, |
13 | | - }); |
14 | | - |
15 | 3 | const enableEmmet = (editor, monaco) => { |
16 | 4 | if (!editor) { |
17 | 5 | throw new Error('Must provide monaco-editor instance.'); |
18 | 6 | } |
19 | 7 |
|
20 | | - editor.addAction({ |
21 | | - // An unique identifier of the contributed action. |
22 | | - id: 'emmet-abbr', |
| 8 | + let cursor; |
| 9 | + let emmetText; |
| 10 | + let expandText; |
23 | 11 |
|
24 | | - // A label of the action that will be presented to the user. |
25 | | - label: 'Emmet: Expand abbreviation', |
| 12 | + // get a legal emmet from a string |
| 13 | + // if whole string matches emmet rules, return it |
| 14 | + // if a substring(right to left) split by white space matches emmet rules, return the substring |
| 15 | + // if nothing matches, return empty string |
| 16 | + const getLegalEmmet = str => { |
| 17 | + // empty or ends with white space, illegal |
| 18 | + if (str === '' || str.match(/\s$/)) return ''; |
26 | 19 |
|
27 | | - // An optional array of keybindings for the action. |
28 | | - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_E], // eslint-disable-line no-bitwise |
| 20 | + // deal with white space, this determines how many characters needed to be emmeted |
| 21 | + // e.g. `a span div` => `a span <div></div>` skip `a span ` |
| 22 | + // e.g. `a{111 222}` => `<a href="">111 222</a>` |
| 23 | + // conclusion: white spaces are only allowed between `[]` or `{}` |
| 24 | + // note: quotes also allowed white spaces, but quotes must in `[]` or `{}`, so ignore it |
| 25 | + const step = { '{': 1, '}': -1, '[': 1, ']': -1 }; |
| 26 | + let pair = 0; |
29 | 27 |
|
30 | | - // A precondition for this action. |
31 | | - precondition: null, |
| 28 | + for (let i = str.length - 1; i > 0; i--) { |
| 29 | + pair += step[str[i]] || 0; |
| 30 | + if (str[i].match(/\s/) && pair >= 0) { |
| 31 | + // illegal white space detected |
| 32 | + str = str.substr(i + 1); // eslint-disable-line |
| 33 | + break; |
| 34 | + } |
| 35 | + } |
32 | 36 |
|
33 | | - // A rule to evaluate on top of the precondition in order to dispatch the keybindings. |
34 | | - keybindingContext: null, |
| 37 | + // starts with illegal character |
| 38 | + // note: emmet self allowed number element like `<1></1>`, |
| 39 | + // but obviously its not fit html standard, so skip it |
| 40 | + if (!str.match(/^[a-zA-Z[(.#]/)) { |
| 41 | + return ''; |
| 42 | + } |
35 | 43 |
|
36 | | - contextMenuGroupId: 'navigation', |
| 44 | + // finally run expand to test the final result |
| 45 | + try { |
| 46 | + expandText = expand(str, { |
| 47 | + addons: { |
| 48 | + jsx: true, |
| 49 | + }, |
| 50 | + }); |
| 51 | + } catch (e) { |
| 52 | + return ''; |
| 53 | + } |
37 | 54 |
|
38 | | - contextMenuOrder: 1.5, |
| 55 | + return str; |
| 56 | + }; |
39 | 57 |
|
40 | | - // Method that will be executed when the action is triggered. |
41 | | - // @param editor The editor instance is passed in as a convenience |
42 | | - run: () => { |
43 | | - let word = editor.model.getValueInRange(editor.getSelection()); |
44 | | - const pos = editor.getPosition(); |
45 | | - if (!word) { |
46 | | - const lineContent = editor.model.getLineContent(pos.lineNumber); |
47 | | - word = extractAbbreviation(lineContent.substring(0, pos.column)); |
48 | | - } |
49 | | - if (word) { |
50 | | - // Get expand text |
51 | | - const expandText = expandAbbreviation(word, 'html'); |
52 | | - if (expandText) { |
53 | | - // replace range content: pos.column , pos.column -word.length; |
54 | | - const range = new monaco.Range( |
55 | | - pos.lineNumber, |
56 | | - pos.column - word.abbreviation.length, |
57 | | - pos.lineNumber, |
58 | | - pos.column |
59 | | - ); |
60 | | - const id = { major: 1, minor: 1 }; |
61 | | - const op = { |
62 | | - identifier: id, |
63 | | - range, |
64 | | - text: expandText, |
65 | | - forceMoveMarkers: true, |
66 | | - }; |
67 | | - editor.executeEdits('', [op]); |
68 | | - return null; |
69 | | - } |
70 | | - return false; |
| 58 | + // register a context key to make sure emmet triggered at proper condition |
| 59 | + const emmetLegal = editor.createContextKey('emmetLegal', false); |
| 60 | + |
| 61 | + editor.onDidChangeCursorPosition(cur => { |
| 62 | + // to ensure emmet triggered at the right time |
| 63 | + // we need to do grammar analysis |
| 64 | + |
| 65 | + const model = editor.model; |
| 66 | + cursor = cur.position; |
| 67 | + |
| 68 | + const column = cursor.column; |
| 69 | + // there is no character before column 1 |
| 70 | + // no need to continue |
| 71 | + if (column === 1) { |
| 72 | + emmetLegal.set(false); |
| 73 | + return; |
| 74 | + } |
| 75 | + |
| 76 | + const lineNumber = cursor.lineNumber; |
| 77 | + |
| 78 | + /* eslint-disable no-underscore-dangle */ |
| 79 | + |
| 80 | + // force line's state to be accurate |
| 81 | + model.getLineTokens(lineNumber, /* inaccurateTokensAcceptable */ false); |
| 82 | + // get the tokenization state at the beginning of this line |
| 83 | + const state = model._lines[lineNumber - 1].getState(); |
| 84 | + // deal with state got null when paste |
| 85 | + if (!state) return; |
| 86 | + |
| 87 | + const freshState = state.clone(); |
| 88 | + // get the human readable tokens on this line |
| 89 | + const token = model._tokenizationSupport.tokenize( |
| 90 | + model.getLineContent(lineNumber), |
| 91 | + freshState, |
| 92 | + 0 |
| 93 | + ).tokens; |
| 94 | + |
| 95 | + /* eslint-enable */ |
| 96 | + |
| 97 | + // get token type at current cursor position |
| 98 | + let i; |
| 99 | + for (i = token.length - 1; i >= 0; i--) { |
| 100 | + if (column - 1 > token[i].offset) { |
| 101 | + break; |
71 | 102 | } |
72 | | - return false; |
73 | | - }, |
| 103 | + } |
| 104 | + |
| 105 | + // type must be empty string when start emmet |
| 106 | + // and if not the first token, make sure the previous token is `delimiter.html` |
| 107 | + // to prevent emmet triggered within attributes |
| 108 | + if ( |
| 109 | + token[i].type !== '' || |
| 110 | + (i > 0 && token[i - 1].type !== 'delimiter.html') |
| 111 | + ) { |
| 112 | + emmetLegal.set(false); |
| 113 | + return; |
| 114 | + } |
| 115 | + |
| 116 | + // get content starts from current token offset to current cursor column |
| 117 | + emmetText = model |
| 118 | + .getLineContent(lineNumber) |
| 119 | + .substring(token[i].offset, column - 1) |
| 120 | + .trimLeft(); |
| 121 | + |
| 122 | + emmetText = getLegalEmmet(emmetText); |
| 123 | + emmetLegal.set(!!emmetText); |
74 | 124 | }); |
| 125 | + |
| 126 | + // add tab command with context |
| 127 | + editor.addCommand( |
| 128 | + monaco.KeyCode.Tab, |
| 129 | + () => { |
| 130 | + // attention: push an undo stop before and after executeEdits |
| 131 | + // to make sure the undo operation is as expected |
| 132 | + editor.pushUndoStop(); |
| 133 | + |
| 134 | + // record first `${0}` position and remove all `${0}` |
| 135 | + /* eslint-disable no-template-curly-in-string */ |
| 136 | + const posOffsetArr = expandText.split('${0}')[0].split('\n'); |
| 137 | + /* eslint-enable */ |
| 138 | + const lineNumber = cursor.lineNumber + posOffsetArr.length - 1; |
| 139 | + const column = |
| 140 | + posOffsetArr.length === 1 |
| 141 | + ? posOffsetArr[0].length - emmetText.length + cursor.column |
| 142 | + : posOffsetArr.slice(-1)[0].length + 1; |
| 143 | + expandText = expandText.replace(/\$\{0\}/g, ''); |
| 144 | + |
| 145 | + // replace range text with expandText |
| 146 | + editor.executeEdits('emmet', [ |
| 147 | + { |
| 148 | + identifier: { major: 1, minor: 1 }, |
| 149 | + range: new monaco.Range( |
| 150 | + cursor.lineNumber, |
| 151 | + cursor.column - emmetText.length, |
| 152 | + cursor.lineNumber, |
| 153 | + cursor.column |
| 154 | + ), |
| 155 | + text: expandText, |
| 156 | + forceMoveMarkers: true, |
| 157 | + }, |
| 158 | + ]); |
| 159 | + |
| 160 | + // move cursor to the position of first `${0}` in expandText |
| 161 | + editor.setPosition(new monaco.Position(lineNumber, column)); |
| 162 | + |
| 163 | + editor.pushUndoStop(); |
| 164 | + }, |
| 165 | + 'emmetLegal && !suggestWidgetVisible' |
| 166 | + ); |
75 | 167 | }; |
76 | 168 |
|
77 | 169 | export default enableEmmet; |
0 commit comments