mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-02 14:31:31 +01:00
Get emmet completions from css extension (#41652)
* Get emmet completions from html,css extensions * Resolve extensionsPath, use emmet results even if empty * Support css abbr with : * Refactoring * Add some basic emmet tests * Refactoring * More tests
This commit is contained in:
1
extensions/css/server/.vscode/launch.json
vendored
1
extensions/css/server/.vscode/launch.json
vendored
@@ -7,6 +7,7 @@
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 6044,
|
||||
"protocol": "inspector",
|
||||
"sourceMaps": true,
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"]
|
||||
},
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-css-languageservice": "^3.0.5",
|
||||
"vscode-languageserver": "^3.5.0"
|
||||
"vscode-languageserver": "^4.0.0-next.4",
|
||||
"vscode-emmet-helper": "1.1.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "2.2.33",
|
||||
"@types/node": "7.0.43"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -20,6 +22,7 @@
|
||||
"install-service-next": "yarn add vscode-css-languageservice@next",
|
||||
"install-service-local": "npm install ../../../../vscode-css-languageservice -f",
|
||||
"install-server-next": "yarn add vscode-languageserver@next",
|
||||
"install-server-local": "npm install ../../../../vscode-languageserver-node/server -f"
|
||||
"install-server-local": "npm install ../../../../vscode-languageserver-node/server -f",
|
||||
"test": "../../../node_modules/.bin/mocha"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,26 @@
|
||||
'use strict';
|
||||
|
||||
import {
|
||||
createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities
|
||||
createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, CompletionTriggerKind
|
||||
} from 'vscode-languageserver';
|
||||
|
||||
import { TextDocument } from 'vscode-languageserver-types';
|
||||
import { TextDocument, CompletionList } from 'vscode-languageserver-types';
|
||||
|
||||
import { ConfigurationRequest } from 'vscode-languageserver-protocol/lib/protocol.configuration.proposed';
|
||||
import { WorkspaceFolder } from 'vscode-languageserver-protocol/lib/protocol.workspaceFolders.proposed';
|
||||
import { DocumentColorRequest, ServerCapabilities as CPServerCapabilities, ColorPresentationRequest } from 'vscode-languageserver-protocol/lib/protocol.colorProvider.proposed';
|
||||
|
||||
import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet } from 'vscode-css-languageservice';
|
||||
import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, ICompletionParticipant } from 'vscode-css-languageservice';
|
||||
import { getLanguageModelCache } from './languageModelCache';
|
||||
import { formatError, runSafe } from './utils/errors';
|
||||
import { doComplete as emmetDoComplete, updateExtensionsPath as updateEmmetExtensionsPath, getEmmetCompletionParticipants } from 'vscode-emmet-helper';
|
||||
import uri from 'vscode-uri';
|
||||
|
||||
export interface Settings {
|
||||
css: LanguageSettings;
|
||||
less: LanguageSettings;
|
||||
scss: LanguageSettings;
|
||||
emmet: { [key: string]: any };
|
||||
}
|
||||
|
||||
// Create a connection for the server.
|
||||
@@ -49,9 +53,22 @@ connection.onShutdown(() => {
|
||||
});
|
||||
|
||||
let scopedSettingsSupport = false;
|
||||
let workspaceFolders: WorkspaceFolder[] | undefined;
|
||||
let emmetSettings = {};
|
||||
let currentEmmetExtensionsPath: string;
|
||||
const emmetTriggerCharacters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
|
||||
// After the server has started the client sends an initilize request. The server receives
|
||||
// in the passed params the rootPath of the workspace plus the client capabilities.
|
||||
connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
workspaceFolders = (<any>params).workspaceFolders;
|
||||
if (!Array.isArray(workspaceFolders)) {
|
||||
workspaceFolders = [];
|
||||
if (params.rootPath) {
|
||||
workspaceFolders.push({ name: '', uri: uri.file(params.rootPath).toString() });
|
||||
}
|
||||
}
|
||||
|
||||
function hasClientCapability(name: string) {
|
||||
let keys = name.split('.');
|
||||
let c: any = params.capabilities;
|
||||
@@ -65,7 +82,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
let capabilities: ServerCapabilities & CPServerCapabilities = {
|
||||
// Tell the client that the server works in FULL text document sync mode
|
||||
textDocumentSync: documents.syncKind,
|
||||
completionProvider: snippetSupport ? { resolveProvider: false } : undefined,
|
||||
completionProvider: snippetSupport ? { resolveProvider: false, triggerCharacters: emmetTriggerCharacters } : undefined,
|
||||
hoverProvider: true,
|
||||
documentSymbolProvider: true,
|
||||
referencesProvider: true,
|
||||
@@ -124,6 +141,13 @@ function updateConfiguration(settings: Settings) {
|
||||
documentSettings = {};
|
||||
// Revalidate any open text documents
|
||||
documents.all().forEach(triggerValidation);
|
||||
|
||||
emmetSettings = settings.emmet;
|
||||
if (currentEmmetExtensionsPath !== emmetSettings['extensionsPath']) {
|
||||
currentEmmetExtensionsPath = emmetSettings['extensionsPath'];
|
||||
const workspaceUri = (workspaceFolders && workspaceFolders.length === 1) ? uri.parse(workspaceFolders[0].uri) : null;
|
||||
updateEmmetExtensionsPath(currentEmmetExtensionsPath, workspaceUri ? workspaceUri.fsPath : null);
|
||||
}
|
||||
}
|
||||
|
||||
let pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
|
||||
@@ -169,11 +193,39 @@ function validateTextDocument(textDocument: TextDocument): void {
|
||||
});
|
||||
}
|
||||
|
||||
let cachedCompletionList: CompletionList;
|
||||
connection.onCompletion(textDocumentPosition => {
|
||||
return runSafe(() => {
|
||||
let document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
let stylesheet = stylesheets.get(document);
|
||||
return getLanguageService(document).doComplete(document, textDocumentPosition.position, stylesheet)!; /* TODO: remove ! once LS has null annotations */
|
||||
if (cachedCompletionList
|
||||
&& !cachedCompletionList.isIncomplete
|
||||
&& textDocumentPosition.context
|
||||
&& textDocumentPosition.context.triggerKind === CompletionTriggerKind.TriggerForIncompleteCompletions
|
||||
) {
|
||||
let result: CompletionList = emmetDoComplete(document, textDocumentPosition.position, document.languageId, emmetSettings);
|
||||
if (result && result.items) {
|
||||
result.items.push(...cachedCompletionList.items);
|
||||
} else {
|
||||
result = cachedCompletionList;
|
||||
cachedCompletionList = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
cachedCompletionList = null;
|
||||
let emmetCompletionList: CompletionList = {
|
||||
isIncomplete: true,
|
||||
items: undefined
|
||||
};
|
||||
const emmetCompletionParticipant: ICompletionParticipant = getEmmetCompletionParticipants(document, textDocumentPosition.position, document.languageId, emmetSettings, emmetCompletionList);
|
||||
getLanguageService(document).setCompletionParticipants([emmetCompletionParticipant]);
|
||||
|
||||
const result = getLanguageService(document).doComplete(document, textDocumentPosition.position, stylesheets.get(document))!; /* TODO: remove ! once LS has null annotations */
|
||||
if (emmetCompletionList && emmetCompletionList.items) {
|
||||
cachedCompletionList = result;
|
||||
return { isIncomplete: true, items: [...emmetCompletionList.items, ...result.items] };
|
||||
}
|
||||
return result;
|
||||
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`);
|
||||
});
|
||||
|
||||
|
||||
68
extensions/css/server/src/test/emmet.test.ts
Normal file
68
extensions/css/server/src/test/emmet.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { getCSSLanguageService, getSCSSLanguageService } from 'vscode-css-languageservice/lib/cssLanguageService';
|
||||
import { TextDocument, CompletionList } from 'vscode-languageserver-types';
|
||||
import { getEmmetCompletionParticipants } from 'vscode-emmet-helper';
|
||||
|
||||
suite('Emmet Support', () => {
|
||||
|
||||
const cssLanguageService = getCSSLanguageService();
|
||||
const scssLanguageService = getSCSSLanguageService();
|
||||
|
||||
function assertCompletions(syntax: string, value: string, expectedProposal: string, expectedProposalDoc: string): void {
|
||||
const offset = value.indexOf('|');
|
||||
value = value.substr(0, offset) + value.substr(offset + 1);
|
||||
|
||||
const document = TextDocument.create('test://test/test.' + syntax, syntax, 0, value);
|
||||
const position = document.positionAt(offset);
|
||||
const emmetCompletionList: CompletionList = {
|
||||
isIncomplete: true,
|
||||
items: undefined
|
||||
}
|
||||
const languageService = syntax === 'scss' ? scssLanguageService : cssLanguageService;
|
||||
languageService.setCompletionParticipants([getEmmetCompletionParticipants(document, position, document.languageId, {}, emmetCompletionList)])
|
||||
const stylesheet = languageService.parseStylesheet(document);
|
||||
const list = languageService.doComplete!(document, position, stylesheet);
|
||||
|
||||
assert.ok(list);
|
||||
assert.ok(emmetCompletionList)
|
||||
|
||||
if (expectedProposal && expectedProposalDoc) {
|
||||
let actualLabels = emmetCompletionList!.items.map(c => c.label).sort();
|
||||
let actualDocs = emmetCompletionList!.items.map(c => c.documentation).sort();
|
||||
assert.ok(actualLabels.indexOf(expectedProposal) !== -1, 'Not found:' + expectedProposal + ' is ' + actualLabels.join(', '));
|
||||
assert.ok(actualDocs.indexOf(expectedProposalDoc) !== -1, 'Not found:' + expectedProposalDoc + ' is ' + actualDocs.join(', '));
|
||||
} else {
|
||||
assert.ok(!emmetCompletionList || !emmetCompletionList.items);
|
||||
}
|
||||
}
|
||||
|
||||
test('Css Emmet Completions', function (): any {
|
||||
assertCompletions('css', '.foo { display: none; m10| }', 'margin: 10px;', 'margin: 10px;');
|
||||
assertCompletions('css', 'foo { display: none; pos:f| }', 'position: fixed;', 'position: fixed;');
|
||||
assertCompletions('css', 'foo { display: none; margin: a| }', null, null);
|
||||
assertCompletions('css', 'foo| { display: none; }', null, null);
|
||||
assertCompletions('css', 'foo {| display: none; }', null, null);
|
||||
assertCompletions('css', 'foo { display: none;| }', null, null);
|
||||
assertCompletions('css', 'foo { display: none|; }', null, null);
|
||||
assertCompletions('css', '.foo { display: none; -m-m10| }', 'margin: 10px;', '-moz-margin: 10px;\nmargin: 10px;');
|
||||
});
|
||||
|
||||
test('Scss Emmet Completions', function (): any {
|
||||
assertCompletions('scss', '.foo { display: none; .bar { m10| } }', 'margin: 10px;', 'margin: 10px;');
|
||||
assertCompletions('scss', 'foo { display: none; .bar { pos:f| } }', 'position: fixed;', 'position: fixed;');
|
||||
assertCompletions('scss', 'foo { display: none; margin: a| .bar {}}', null, null);
|
||||
assertCompletions('scss', 'foo| { display: none; }', null, null);
|
||||
assertCompletions('scss', 'foo {| display: none; }', null, null);
|
||||
assertCompletions('scss', 'foo { display: none;| }', null, null);
|
||||
assertCompletions('scss', 'foo { display: none|; }', null, null);
|
||||
assertCompletions('scss', '.foo { display: none; -m-m10| }', 'margin: 10px;', '-moz-margin: 10px;\nmargin: 10px;');
|
||||
});
|
||||
|
||||
});
|
||||
3
extensions/css/server/test/mocha.opts
Normal file
3
extensions/css/server/test/mocha.opts
Normal file
@@ -0,0 +1,3 @@
|
||||
--ui tdd
|
||||
--useColors true
|
||||
./out/test
|
||||
@@ -2,10 +2,22 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@emmetio/extract-abbreviation@^0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@emmetio/extract-abbreviation/-/extract-abbreviation-0.1.4.tgz#f5be070db97901ecc37e5204f2ace68242cdcefa"
|
||||
|
||||
"@types/mocha@2.2.33":
|
||||
version "2.2.33"
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.33.tgz#d79a0061ec270379f4d9e225f4096fb436669def"
|
||||
|
||||
"@types/node@7.0.43":
|
||||
version "7.0.43"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c"
|
||||
|
||||
jsonc-parser@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-1.0.0.tgz#ddcc864ae708e60a7a6dd36daea00172fa8d9272"
|
||||
|
||||
vscode-css-languageservice@^3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-3.0.5.tgz#8470989c07bbe740db4fa621423e98384d2c342f"
|
||||
@@ -13,26 +25,38 @@ vscode-css-languageservice@^3.0.5:
|
||||
vscode-languageserver-types "3.5.0"
|
||||
vscode-nls "^2.0.1"
|
||||
|
||||
vscode-jsonrpc@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.5.0.tgz#87239d9e166b2d7352245b8a813597804c1d63aa"
|
||||
|
||||
vscode-languageserver-protocol@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.5.0.tgz#067c5cbe27709795398d119692c97ebba1452209"
|
||||
vscode-emmet-helper@1.1.34:
|
||||
version "1.1.34"
|
||||
resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-1.1.34.tgz#1e2d3e26c389efc6f73ceb4c0ed797ae07291874"
|
||||
dependencies:
|
||||
vscode-jsonrpc "^3.5.0"
|
||||
vscode-languageserver-types "^3.5.0"
|
||||
"@emmetio/extract-abbreviation" "^0.1.4"
|
||||
jsonc-parser "^1.0.0"
|
||||
vscode-languageserver-types "^3.6.0-next.1"
|
||||
|
||||
vscode-languageserver-types@3.5.0, vscode-languageserver-types@^3.5.0:
|
||||
vscode-jsonrpc@^3.6.0-next.1:
|
||||
version "3.6.0-next.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.6.0-next.1.tgz#3cb463dffe5842d6aec16718ca9252708cd6aabe"
|
||||
|
||||
vscode-languageserver-protocol@^3.6.0-next.5:
|
||||
version "3.6.0-next.5"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.6.0-next.5.tgz#ed2ec2db759826f753c0a13977dfb2bedc4d31b3"
|
||||
dependencies:
|
||||
vscode-jsonrpc "^3.6.0-next.1"
|
||||
vscode-languageserver-types "^3.6.0-next.1"
|
||||
|
||||
vscode-languageserver-types@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.5.0.tgz#e48d79962f0b8e02de955e3f524908e2b19c0374"
|
||||
|
||||
vscode-languageserver@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-3.5.0.tgz#d28099bc6ddda8c1dd16b707e454e1b1ddae0dba"
|
||||
vscode-languageserver-types@^3.6.0-next.1:
|
||||
version "3.6.0-next.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.6.0-next.1.tgz#98e488d3f87b666b4ee1a3d89f0023e246d358f3"
|
||||
|
||||
vscode-languageserver@^4.0.0-next.4:
|
||||
version "4.0.0-next.4"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-4.0.0-next.4.tgz#162440b15bedaab07e1676f046e4d9b8578b3d92"
|
||||
dependencies:
|
||||
vscode-languageserver-protocol "^3.5.0"
|
||||
vscode-languageserver-protocol "^3.6.0-next.5"
|
||||
vscode-uri "^1.0.1"
|
||||
|
||||
vscode-nls@^2.0.1:
|
||||
|
||||
Reference in New Issue
Block a user