alm
Version:
The best IDE for TypeScript
255 lines (254 loc) • 9.01 kB
JavaScript
"use strict";
/**
* inspiration:
* https://github.com/formulahendry/vscode-auto-close-tag/blob/5921f24ffc6fc9350e1ce7c2a74ea99fab0c5b11/src/extension.ts
* Modified to:
* - remove options. We are in sublime mode, no excluded tags etc
* - work with monaco instead of a vscode workspace
*/
Object.defineProperty(exports, "__esModule", { value: true });
var events_1 = require("../../../common/events");
var monacoUtils = require("../monacoUtils");
/**
* We want to disable it e.g. when auto writing code
*/
var enabled = true;
exports.disableAutoClose = function () { return enabled = false; };
exports.enableAutoClose = function () { return enabled = true; };
function setup(cm) {
var disposible = new events_1.CompositeDisposible();
disposible.add(cm.onDidChangeModelContent(function (e) {
if (!enabled)
return;
/** Close tag */
insertAutoCloseTag(e, cm);
}));
return disposible;
}
exports.setup = setup;
function insertAutoCloseTag(event, editor) {
var originalRange = event.range;
/** User just did `<foo>` */
if (event.text === ">") {
var text = editor.getModel().getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: originalRange.endLineNumber,
endColumn: originalRange.endColumn,
});
/**
* Check that its not
* `/>` (self closing)
* `=>` (arrow)
* `}>` (generic) ... hard cause `<foo bar={someting}>` is valid :-/
* By just checking is a char
**/
var lastChar = "";
if (text.length > 2) {
lastChar = text.substr(text.length - 1);
}
if (lastChar === "/" || lastChar === '=') {
return;
}
var closeTag = getCloseTagIfAtAnOpenOne(editor.filePath, editor.getModel().getOffsetAt({
lineNumber: originalRange.endLineNumber,
column: originalRange.endColumn
}));
if (!closeTag)
return;
closeTag = "</" + closeTag + ">";
/** Make edits */
var startAt = editor.getModel().modifyPosition({
lineNumber: originalRange.endLineNumber,
column: originalRange.endColumn
}, 1);
monacoUtils.replaceRange({
model: editor.getModel(),
range: {
startLineNumber: startAt.lineNumber,
startColumn: startAt.column,
endLineNumber: startAt.lineNumber,
endColumn: startAt.column
},
newText: closeTag
});
return;
}
/** User just did `<foo>something</` */
if (event.text === "/") {
var text = editor.getModel().getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: originalRange.endLineNumber,
endColumn: originalRange.endColumn,
});
var lastChar = "";
if (text.length > 2) {
lastChar = text.substr(text.length - 1);
}
if (lastChar !== "<") {
return;
}
/** Yay, we have </ See if we have a close tag ? */
var closeTag = getCloseTagForAnAlreadyOpenOne(editor.filePath, editor.getModel().getOffsetAt({
lineNumber: originalRange.endLineNumber,
column: originalRange.endColumn
}));
if (!closeTag) {
return;
}
/** Yay we have candidate closeTag like `div` */
/**
* If the user already has a trailing `>` e.g.
* before: <div><(pos)>
* after: <div><(pos)/>
* Next chars will be `/>`
*/
var nextChars = getNext2Chars(editor, { lineNumber: originalRange.endLineNumber, column: originalRange.endColumn });
/** If the next chars are not `/>` then we want to complete `>` for the user as well */
if (nextChars !== "/>") {
closeTag = closeTag + '>';
}
/** Make edits */
var startAt = editor.getModel().modifyPosition({
lineNumber: originalRange.endLineNumber,
column: originalRange.endColumn
}, 1);
monacoUtils.replaceRange({
model: editor.getModel(),
range: {
startLineNumber: startAt.lineNumber,
startColumn: startAt.column,
endLineNumber: startAt.lineNumber,
endColumn: startAt.column
},
newText: closeTag
});
/** And advance the cursor */
var endAt_1 = editor.getModel().modifyPosition({
lineNumber: startAt.lineNumber,
column: startAt.column
}, closeTag.length);
if (nextChars === "/>") {
/** Advance one char more */
endAt_1 = editor.getModel().modifyPosition(endAt_1, 1);
}
/** Set timeout. Because it doesn't work otherwise */
setTimeout(function () {
editor.setSelection({
startLineNumber: endAt_1.lineNumber,
startColumn: endAt_1.column,
endLineNumber: endAt_1.lineNumber,
endColumn: endAt_1.column,
});
});
}
}
function getNext2Chars(editor, position) {
var nextPos = editor.getModel().modifyPosition(position, 2);
var text = editor.getModel().getValueInRange({
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: nextPos.lineNumber,
endColumn: nextPos.column,
});
return text;
}
var classifierCache_1 = require("../model/classifierCache");
function getCloseTagForAnAlreadyOpenOne(filePath, position) {
var sourceFile = classifierCache_1.getSourceFile(filePath);
var opens = [];
var collectTags = function (node) {
if (ts.isJsxOpeningElement(node)) {
if (node.getStart() >= position)
return;
if (node.getStart() === (position - 1)) {
/**
* This is actually just
* <div><>
* ^ parsed as an opening
*/
return;
}
opens.push(node);
}
if (ts.isJsxClosingElement(node)) {
if (node.getStart() >= position)
return;
opens.pop();
}
ts.forEachChild(node, collectTags);
};
ts.forEachChild(sourceFile, collectTags);
// console.log(opens.map(o => o.getFullText())); // DEBUG
if (opens.length) {
var tabToClose = opens[opens.length - 1]; // close the last one first
var tagName = tabToClose.tagName.getText(); // something like `foo.Someting`
return tagName;
}
return null;
}
function getCloseTagIfAtAnOpenOne(filePath, position) {
var sourceFile = classifierCache_1.getSourceFile(filePath);
var found = null;
var collectTags = function (node) {
/**
* <div
* Is actually parsed as a JSX self closing tag
**/
if (node.kind === ts.SyntaxKind.JsxSelfClosingElement) {
/**
* With
* <foo>
* <another(cursor)
* </foo>
*
* the `<another </` is what the jsx self closing tag contains
* So if its a *self closing* tag with a start before and end after ... its a candidate
*/
if (!(node.getStart() <= position) || !(node.getEnd() >= position))
return;
var fullText = node.getFullText().trim();
// is actually closed
if (fullText.endsWith('/')
/**
* Is getting the next `</` e.g.
* <foo>
* <another(cursor)
* </foo>
* is parsed as
* `<another </`
*/
&& !fullText.endsWith('</'))
return;
if (found) {
var previous = found;
/** If it surrounds the position more tightly */
var delta = (position - node.getStart()) + (node.getEnd() - position);
var previousDelta = (position - previous.getStart()) + (previous.getEnd() - position);
if (delta < previousDelta) {
found = node;
;
}
}
else {
found = node;
}
}
ts.forEachChild(node, collectTags);
};
ts.forEachChild(sourceFile, collectTags);
if (found) {
/**
* For
* <div|Hello
* We want <div only. But tag name full text gives us `<divHello`.
* So fix it by simply only giving `<div` text before position
*/
var tagName = found.tagName;
var start = found.tagName.getStart();
var end = position;
return sourceFile.getFullText().substring(start, end);
}
return null;
}