Skip to content
This repository was archived by the owner on Apr 4, 2023. It is now read-only.

Emmet in typescript styled plugin #33

Merged
merged 7 commits into from
Feb 16, 2018
Merged
Show file tree
Hide file tree
Changes from 3 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
148 changes: 148 additions & 0 deletions e2e/tests/emmetCompletions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
const assert = require('chai').assert;
const createServer = require('../server-fixture');
const { openMockFile, getFirstResponseOfType } = require('./_helpers');

const mockFileName = 'main.ts';

describe('Completions', () => {
it('should return emmet property completions for single line string', () => {
const server = createServer();
openMockFile(server, mockFileName, 'const q = css`m10-20`');
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 21, line: 1 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === 'margin: 10px 20px;'));
});
});


it('should return emmet property completions for multiline string', () => {
const server = createServer();
openMockFile(server, mockFileName, [
'const q = css`',
'm10-20',
'`'
].join('\n'));
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 22, line: 1 } });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Use the actual line numbers for the tests


return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === 'margin: 10px 20px;'));
});
});

it('should return emmet property completions for nested selector', () => {
const server = createServer();
openMockFile(server, mockFileName, 'const q = css`position: relative; &:hover { m10-20 }`');
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 51, line: 1 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === 'margin: 10px 20px;'));
});
});

it('should return emmet completions when placeholder is used as property', () => {
const server = createServer();
openMockFile(server, mockFileName, 'css`m10-20 ; boarder: 1px solid ${"red"};`');
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 11, line: 1 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === 'margin: 10px 20px;'));
});
});

it('should return emmet completions after where placeholder is used as property', () => {
const server = createServer();
openMockFile(server, mockFileName, 'css`border: 1px solid ${"red"}; m10-20`');
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 39, line: 1 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === 'margin: 10px 20px;'));
});
});

it('should return emmet completions between were placeholders are used as properties', () => {
const server = createServer();
openMockFile(server, mockFileName, 'css`boarder: 1px solid ${"red"}; color: #12; margin: ${20}; `')
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 44, line: 1 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === '#121212'));
});
});

it('should return emmet completions on tagged template string with placeholder using dotted tag', () => {
const server = createServer();
openMockFile(server, mockFileName, 'css.x`color: #12 ; boarder: 1px solid ${"red"};`');
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 17, line: 1 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.success);
assert.isTrue(completionsResponse.body.some(item => item.name === '#121212'));
});
});

it('should return styled emmet completions inside of nested placeholder', () => {
const server = createServer();
openMockFile(server, mockFileName, 'styled`background: red; ${(() => css`color: #12`)()}`;');
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 48, line: 1 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === '#121212'));
});
});

it('should handle emmet completions in multiline value placeholder correctly ', () => {
const server = createServer();
openMockFile(server, mockFileName, [
'css`margin: ${',
'0',
"}; color: #12`"].join('\n'));
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 14, line: 3 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === 'aliceblue'));
});
});

it('should handle emmet completions in multiline rule placeholder correctly ', () => {
const server = createServer();
openMockFile(server, mockFileName, [
'css`',
'${',
'css`margin: 0;`',
'}',
'color: #12`'].join('\n'));
server.send({ command: 'completions', arguments: { file: mockFileName, offset: 11, line: 5 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === '#121212'));
});
});

it('should return emmet completions inside of nested selector xx', () => {
const server = createServer();
openMockFile(server, mockFileName, [
'css`',
' color: red;',
' &:hover {',
' color: #12 ',
' }',
'`'].join('\n'));
server.send({ command: 'completions', arguments: { file: mockFileName, line: 4, offset: 19 } });

return server.close().then(() => {
const completionsResponse = getFirstResponseOfType('completions', server);
assert.isTrue(completionsResponse.body.some(item => item.name === '#121212'));
});
});
})
35 changes: 31 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"dependencies": {
"typescript-template-language-service-decorator": "^1.2.0",
"vscode-css-languageservice": "^3.0.5",
"vscode-languageserver-types": "^3.5.0"
"vscode-languageserver-types": "^3.5.0",
"vscode-emmet-helper": "1.1.36"
},
"files": [
"lib"
Expand Down
15 changes: 14 additions & 1 deletion src/styled-template-language-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as config from './config';
import { TsStyledPluginConfiguration } from './configuration';
import { TemplateLanguageService, TemplateContext } from 'typescript-template-language-service-decorator';
import { LanguageServiceLogger } from './logger';
import { doComplete as emmetDoComplete, getEmmetCompletionParticipants} from 'vscode-emmet-helper';

const wrapperPre = ':root{\n';

Expand Down Expand Up @@ -240,13 +241,22 @@ export default class StyledTemplateLanguageService implements TemplateLanguageSe
const doc = this.createVirtualDocument(context);
const virtualPosition = this.toVirtualDocPosition(position);
const stylesheet = this.scssLanguageService.parseStylesheet(doc);
const emmetResults: vscode.CompletionList = {
isIncomplete: true,
items: []
}
this.cssLanguageService.setCompletionParticipants([getEmmetCompletionParticipants(doc, virtualPosition, 'css', {}, emmetResults)]);
const completionsCss = this.cssLanguageService.doComplete(doc, virtualPosition, stylesheet) || emptyCompletionList;
const completionsScss = this.scssLanguageService.doComplete(doc, virtualPosition, stylesheet) || emptyCompletionList;
completionsScss.items = filterScssCompletionItems(completionsScss.items);
const completions: vscode.CompletionList = {
isIncomplete: false,
items: [...completionsCss.items, ...completionsScss.items],
};
if (emmetResults.items.length) {
completions.items.push(...emmetResults.items);
completions.isIncomplete = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a heads up: isIncomplete is ignored by TS so setting it to true won't have any effect. Does emmet depend on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it does. For example: m -> margin: ; and mt -> margin-top: ;. So the completions need to be re-calculated for almost every char

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened microsoft/TypeScript#21999. I've discussed this problem with TS before but emmet may be the first real use case for it.

As we discussed offline, I believe we should be able to take in this PR even without isIncomplete. Users will just have to manually trigger completions in some cases. Do you agree @ramya-rao-a?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

}
this._completionsCache.updateCached(context, position, completions);
return completions;
}
Expand Down Expand Up @@ -466,7 +476,10 @@ function translateSeverity(
}

function toDisplayParts(
text: string | undefined
text: string | vscode.MarkupContent | undefined
): ts.SymbolDisplayPart[] {
if (text && typeof text !== 'string') {
return [{text: text.value, kind: 'text'}];
}
return text ? [{ text, kind: 'text' }] : [];
}