Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial commit for builtin emmet extension #21943 #27176

Merged
merged 1 commit into from
May 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build/npm/postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const extensions = [
'gulp',
'grunt',
'jake',
'merge-conflict'
'merge-conflict',
'emmet'
];

extensions.forEach(extension => npmInstall(`extensions/${extension}`));
Expand Down
70 changes: 70 additions & 0 deletions extensions/emmet/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "emmet",
"displayName": "emmet",
"description": "Emmet support for VS Code",
"version": "0.0.1",
"publisher": "vscode",
"engines": {
"vscode": "^1.10.0"
},
"categories": [
"Other"
],
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/vscode-emmet"
},
"activationEvents": [
"onLanguage:html",
"onLanguage:jade",
"onLanguage:slim",
"onLanguage:haml",
"onLanguage:xml",
"onLanguage:xsl",
"onLanguage:css",
"onLanguage:scss",
"onLanguage:sass",
"onLanguage:less",
"onLanguage:stylus",
"onLanguage:javascriptreact",
"onLanguage:typescriptreact"
],
"main": "./out/extension",
"contributes": {
"configuration": {
"type": "object",
"title": "Emmet configuration",
"properties": {
"emmet.suggestExpandedAbbreviation": {
"type": "boolean",
"default": true,
"description": "Shows expanded emmet abbreviations as suggestions"
},
"emmet.suggestAbbreviations": {
"type": "boolean",
"default": true,
"description": "Shows possible emmet abbreviations as suggestions"
}
}
}
},
"scripts": {
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test"
},
"devDependencies": {
"typescript": "^2.0.3",
"vscode": "^1.0.0",
"mocha": "^2.3.3",
"@types/node": "^6.0.40",
"@types/mocha": "^2.2.32"
},
"dependencies": {
"@emmetio/expand-abbreviation": "^0.5.4",
"@emmetio/extract-abbreviation": "^0.1.1",
"@emmetio/html-matcher": "^0.3.1",
"@emmetio/css-parser": "^0.3.0"
}
}
57 changes: 57 additions & 0 deletions extensions/emmet/src/abbreviationActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { expand } from '@emmetio/expand-abbreviation';
import { getSyntax, getProfile, extractAbbreviation } from './util';

const field = (index, placeholder) => `\${${index}${placeholder ? ':' + placeholder : ''}}`;

export function wrapWithAbbreviation() {
let editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showInformationMessage('No editor is active');
return;
}
let rangeToReplace: vscode.Range = editor.selection;
if (rangeToReplace.isEmpty) {
rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, editor.document.lineAt(rangeToReplace.start.line).text.length);
}
let textToReplace = editor.document.getText(rangeToReplace);
let options = {
field: field,
syntax: getSyntax(editor.document),
profile: getProfile(getSyntax(editor.document)),
text: textToReplace
};

vscode.window.showInputBox({ prompt: 'Enter Abbreviation' }).then(abbr => {
if (!abbr || !abbr.trim()) { return; }
let expandedText = expand(abbr, options);
editor.insertSnippet(new vscode.SnippetString(expandedText), rangeToReplace);
});
}

export function expandAbbreviation() {
let editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showInformationMessage('No editor is active');
return;
}
let rangeToReplace: vscode.Range = editor.selection;
let abbr = editor.document.getText(rangeToReplace);
if (rangeToReplace.isEmpty) {
[rangeToReplace, abbr] = extractAbbreviation(rangeToReplace.start);
}

let options = {
field: field,
syntax: getSyntax(editor.document),
profile: getProfile(getSyntax(editor.document))
};

let expandedText = expand(abbr, options);
editor.insertSnippet(new vscode.SnippetString(expandedText), rangeToReplace);
}
75 changes: 75 additions & 0 deletions extensions/emmet/src/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { getNode, getNodeOuterSelection, getNodeInnerSelection, isStyleSheet } from './util';
import parse from '@emmetio/html-matcher';
import Node from '@emmetio/node';

export function balanceOut() {
balance(true);
}

export function balanceIn() {
balance(false);
}

function balance(out: boolean) {
let editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showInformationMessage('No editor is active');
return;
}
if (isStyleSheet(editor.document.languageId)) {
return;
}
let getRangeFunction = out ? getRangeToBalanceOut : getRangeToBalanceIn;

let rootNode: Node = parse(editor.document.getText());

let newSelections: vscode.Selection[] = [];
editor.selections.forEach(selection => {
let range = getRangeFunction(editor.document, selection, rootNode);
if (range) {
newSelections.push(range);
}
});

editor.selection = newSelections[0];
editor.selections = newSelections;
}

function getRangeToBalanceOut(document: vscode.TextDocument, selection: vscode.Selection, rootNode: Node): vscode.Selection {
let offset = document.offsetAt(selection.start);
let nodeToBalance = getNode(rootNode, offset);

let innerSelection = getNodeInnerSelection(document, nodeToBalance);
let outerSelection = getNodeOuterSelection(document, nodeToBalance);

if (innerSelection.contains(selection) && !innerSelection.isEqual(selection)) {
return innerSelection;
}
if (outerSelection.contains(selection) && !outerSelection.isEqual(selection)) {
return outerSelection;
}
return;
}

function getRangeToBalanceIn(document: vscode.TextDocument, selection: vscode.Selection, rootNode: Node): vscode.Selection {
let offset = document.offsetAt(selection.start);
let nodeToBalance: Node = getNode(rootNode, offset);

if (!nodeToBalance.firstChild) {
return selection;
}

if (nodeToBalance.firstChild.start === offset && nodeToBalance.firstChild.end === document.offsetAt(selection.end)) {
return getNodeInnerSelection(document, nodeToBalance.firstChild);
}

return new vscode.Selection(document.positionAt(nodeToBalance.firstChild.start), document.positionAt(nodeToBalance.firstChild.end));

}

70 changes: 70 additions & 0 deletions extensions/emmet/src/editPoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { validate } from './util';

export function fetchEditPoint(direction: string): void {
let editor = vscode.window.activeTextEditor;
if (!validate()) {
return;
}

let newSelections: vscode.Selection[] = [];
editor.selections.forEach(selection => {
let updatedSelection = direction === 'next' ? nextEditPoint(selection.anchor, editor) : prevEditPoint(selection.anchor, editor);
newSelections.push(updatedSelection);
});
editor.selections = newSelections;
}

function nextEditPoint(position: vscode.Position, editor: vscode.TextEditor): vscode.Selection {
for (let lineNum = position.line; lineNum < editor.document.lineCount; lineNum++) {
let updatedSelection = findEditPoint(lineNum, editor, position, 'next');
if (updatedSelection) {
return updatedSelection;
}
}
}

function prevEditPoint(position: vscode.Position, editor: vscode.TextEditor): vscode.Selection {
for (let lineNum = position.line; lineNum >= 0; lineNum--) {
let updatedSelection = findEditPoint(lineNum, editor, position, 'prev');
if (updatedSelection) {
return updatedSelection;
}
}
}


function findEditPoint(lineNum: number, editor: vscode.TextEditor, position: vscode.Position, direction: string): vscode.Selection {
let line = editor.document.lineAt(lineNum);

if (lineNum !== position.line && line.isEmptyOrWhitespace) {
editor.selection = new vscode.Selection(lineNum, 0, lineNum, 0);
return;
}

let lineContent = line.text;
if (lineNum === position.line && direction === 'prev') {
lineContent = lineContent.substr(0, position.character);
}
let emptyAttrIndex = direction === 'next' ? lineContent.indexOf('""', lineNum === position.line ? position.character : 0) : lineContent.lastIndexOf('""');
let emptyTagIndex = direction === 'next' ? lineContent.indexOf('><', lineNum === position.line ? position.character : 0) : lineContent.lastIndexOf('><');

let winner = -1;

if (emptyAttrIndex > -1 && emptyTagIndex > -1) {
winner = direction === 'next' ? Math.min(emptyAttrIndex, emptyTagIndex) : Math.max(emptyAttrIndex, emptyTagIndex);
} else if (emptyAttrIndex > -1) {
winner = emptyAttrIndex;
} else {
winner = emptyTagIndex;
}

if (winner > -1) {
return new vscode.Selection(lineNum, winner + 1, lineNum, winner + 1);
}
}
105 changes: 105 additions & 0 deletions extensions/emmet/src/emmetCompletionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/


import * as vscode from 'vscode';
import { expand, createSnippetsRegistry } from '@emmetio/expand-abbreviation';
import { getSyntax, isStyleSheet, getProfile, extractAbbreviation } from './util';

const field = (index, placeholder) => `\${${index}${placeholder ? ':' + placeholder : ''}}`;
const snippetCompletionsCache = new Map<string, vscode.CompletionItem[]>();

export class EmmetCompletionItemProvider implements vscode.CompletionItemProvider {

public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable<vscode.CompletionList> {

if (!vscode.workspace.getConfiguration('emmet')['useModules']) {
return Promise.resolve(null);
}

let currentWord = getCurrentWord(document, position);
let expandedAbbr = getExpandedAbbreviation(document, position);
let abbreviationSuggestions = getAbbreviationSuggestions(getSyntax(document), currentWord, (expandedAbbr && currentWord === expandedAbbr.label));
let completionItems = expandedAbbr ? [expandedAbbr, ...abbreviationSuggestions] : abbreviationSuggestions;

return Promise.resolve(new vscode.CompletionList(completionItems, true));
}
}

function getExpandedAbbreviation(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem {
if (!vscode.workspace.getConfiguration('emmet')['suggestExpandedAbbreviation']) {
return;
}
let [rangeToReplace, wordToExpand] = extractAbbreviation(position);
if (!rangeToReplace || !wordToExpand) {
return;
}
let syntax = getSyntax(document);
let expandedWord = expand(wordToExpand, {
field: field,
syntax: syntax,
profile: getProfile(syntax)
});

let completionitem = new vscode.CompletionItem(wordToExpand);
completionitem.insertText = new vscode.SnippetString(expandedWord);
completionitem.documentation = removeTabStops(expandedWord);
completionitem.range = rangeToReplace;
completionitem.detail = 'Expand Emmet Abbreviation';

// In non stylesheet like syntax, this extension returns expanded abbr plus posssible abbr completions
// To differentiate between the 2, the former is given CompletionItemKind.Value so that it gets a different icon
if (!isStyleSheet(syntax)) {
completionitem.kind = vscode.CompletionItemKind.Value;
}
return completionitem;
}

function getCurrentWord(document: vscode.TextDocument, position: vscode.Position): string {
let wordAtPosition = document.getWordRangeAtPosition(position);
let currentWord = '';
if (wordAtPosition && wordAtPosition.start.character < position.character) {
let word = document.getText(wordAtPosition);
currentWord = word.substr(0, position.character - wordAtPosition.start.character);
}

return currentWord;
}

function removeTabStops(expandedWord: string): string {
return expandedWord.replace(/\$\{\d+\}/g, '').replace(/\$\{\d+:([^\}]+)\}/g, '$1');
}
function getAbbreviationSuggestions(syntax: string, prefix: string, skipExactMatch: boolean) {
if (!vscode.workspace.getConfiguration('emmet')['suggestAbbreviations'] || !prefix || isStyleSheet(syntax)) {
return [];
}

if (!snippetCompletionsCache.has(syntax)) {
let registry = createSnippetsRegistry(syntax);
let completions: vscode.CompletionItem[] = registry.all({ type: 'string' }).map(snippet => {
let expandedWord = expand(snippet.value, {
field: field,
syntax: syntax
});

let item = new vscode.CompletionItem(snippet.key);
item.documentation = removeTabStops(expandedWord);
item.detail = 'Complete Emmet Abbreviation';
item.insertText = snippet.key;
return item;
});
snippetCompletionsCache.set(syntax, completions);
}

let snippetCompletions = snippetCompletionsCache.get(syntax);

snippetCompletions = snippetCompletions.filter(x => x.label.startsWith(prefix) && (!skipExactMatch || x.label !== prefix));

return snippetCompletions;

}



Loading