UNPKG

alm

Version:

The best IDE for TypeScript

255 lines (254 loc) 9.01 kB
"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; }