Skip to content

Commit 1eec011

Browse files
author
Ives van Hoorne
committed
Make emmet work on tab
1 parent c6c95eb commit 1eec011

File tree

1 file changed

+150
-58
lines changed

1 file changed

+150
-58
lines changed
Lines changed: 150 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,169 @@
1-
import extractAbbreviation from '@emmetio/extract-abbreviation';
21
import { expand } from '@emmetio/expand-abbreviation';
32

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-
153
const enableEmmet = (editor, monaco) => {
164
if (!editor) {
175
throw new Error('Must provide monaco-editor instance.');
186
}
197

20-
editor.addAction({
21-
// An unique identifier of the contributed action.
22-
id: 'emmet-abbr',
8+
let cursor;
9+
let emmetText;
10+
let expandText;
2311

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 '';
2619

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;
2927

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+
}
3236

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+
}
3543

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+
}
3754

38-
contextMenuOrder: 1.5,
55+
return str;
56+
};
3957

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;
71102
}
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);
74124
});
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+
);
75167
};
76168

77169
export default enableEmmet;

0 commit comments

Comments
 (0)