Skip to content
Open
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
12 changes: 11 additions & 1 deletion extensions/emmet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,17 @@
"emmet.includeLanguages": {
"type": "object",
"additionalProperties": {
"type": "string"
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"default": {},
"markdownDescription": "%emmetIncludeLanguages%"
Expand Down
48 changes: 46 additions & 2 deletions extensions/emmet/src/abbreviationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,11 @@ function expandAbbr(input: ExpandAbbreviationInput): string | undefined {
return expandedText;
}

/**
* Gets the Emmet syntax from command arguments
* @param args Command arguments containing language and parentMode information
* @returns The Emmet syntax string or undefined if not applicable
*/
export function getSyntaxFromArgs(args: { [x: string]: string }): string | undefined {
const mappedModes = getMappingForIncludedLanguages();
const language: string = args['language'];
Expand All @@ -708,9 +713,48 @@ export function getSyntaxFromArgs(args: { [x: string]: string }): string | undef
return;
}

let syntax = getEmmetMode(mappedModes[language] ?? language, mappedModes, excludedLanguages);
let syntax: string | undefined;
const languageMapping = mappedModes[language];

if (languageMapping) {
if (typeof languageMapping === 'string') {
// Handle single string mapping (backward compatibility)
syntax = getEmmetMode(languageMapping, mappedModes, excludedLanguages);
} else if (Array.isArray(languageMapping)) {
// Handle array of languages (new feature)
// Try each language in the array until we find a valid syntax
for (const lang of languageMapping) {
const mode = getEmmetMode(lang, mappedModes, excludedLanguages);
if (mode) {
syntax = mode;
break;
}
}
}
}

if (!syntax) {
syntax = getEmmetMode(mappedModes[parentMode] ?? parentMode, mappedModes, excludedLanguages);
syntax = getEmmetMode(language, mappedModes, excludedLanguages);
}

if (!syntax) {
const parentMapping = mappedModes[parentMode];
if (parentMapping) {
if (typeof parentMapping === 'string') {
syntax = getEmmetMode(parentMapping, mappedModes, excludedLanguages);
} else if (Array.isArray(parentMapping)) {
for (const lang of parentMapping) {
const mode = getEmmetMode(lang, mappedModes, excludedLanguages);
if (mode) {
syntax = mode;
break;
}
}
}
}
if (!syntax) {
syntax = getEmmetMode(parentMode, mappedModes, excludedLanguages);
}
}

return syntax;
Expand Down
31 changes: 29 additions & 2 deletions extensions/emmet/src/defaultCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi
});
}

/**
* Internal method to provide completion items for Emmet abbreviations
* @param document The text document to provide completions for
* @param position The position in the document to provide completions at
* @param context The completion context
* @returns A promise resolving to completion items or undefined
*/
private provideCompletionItemsInternal(document: vscode.TextDocument, position: vscode.Position, context: vscode.CompletionContext): Thenable<vscode.CompletionList | undefined> | undefined {
const emmetConfig = vscode.workspace.getConfiguration('emmet');
const excludedLanguages = emmetConfig['excludeLanguages'] ? emmetConfig['excludeLanguages'] : [];
Expand All @@ -48,8 +55,28 @@ export class DefaultCompletionItemProvider implements vscode.CompletionItemProvi
}

const mappedLanguages = getMappingForIncludedLanguages();
const isSyntaxMapped = mappedLanguages[document.languageId] ? true : false;
const emmetMode = getEmmetMode((isSyntaxMapped ? mappedLanguages[document.languageId] : document.languageId), mappedLanguages, excludedLanguages);
const mapping = mappedLanguages[document.languageId];
const isSyntaxMapped = !!mapping;

let emmetMode: string | undefined;
if (isSyntaxMapped) {
if (typeof mapping === 'string') {
// Handle single string mapping (backward compatibility)
emmetMode = getEmmetMode(mapping, mappedLanguages, excludedLanguages);
} else if (Array.isArray(mapping)) {
// Handle array of languages (new feature)
// Try each language in the array until we find a valid Emmet mode
for (const lang of mapping) {
const mode = getEmmetMode(lang, mappedLanguages, excludedLanguages);
if (mode) {
emmetMode = mode;
break;
}
}
}
} else {
emmetMode = getEmmetMode(document.languageId, mappedLanguages, excludedLanguages);
}

if (!emmetMode
|| emmetConfig['showExpandedAbbreviation'] === 'never'
Expand Down
35 changes: 29 additions & 6 deletions extensions/emmet/src/emmetCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,24 @@ export function activateEmmetExtension(context: vscode.ExtensionContext) {
/**
* Holds any registered completion providers by their language strings
*/
const languageMappingForCompletionProviders: Map<string, string> = new Map<string, string>();
const languageMappingForCompletionProviders: Map<string, string | string[]> = new Map<string, string | string[]>();
const completionProviderDisposables: vscode.Disposable[] = [];

/**
* Helper function to merge trigger characters from multiple languages
* @param languages Array of language identifiers to merge trigger characters from
* @returns Array of unique trigger characters from all specified languages
*/
function mergeTriggerCharacters(languages: string[]): string[] {
const triggerChars = new Set<string>();
languages.forEach(lang => {
if (LANGUAGE_MODES[lang]) {
LANGUAGE_MODES[lang].forEach(char => triggerChars.add(char));
}
});
return Array.from(triggerChars);
}

function refreshCompletionProviders(_: vscode.ExtensionContext) {
clearCompletionProviderInfo();

Expand Down Expand Up @@ -195,19 +210,27 @@ function refreshCompletionProviders(_: vscode.ExtensionContext) {
const useInlineCompletionProvider = vscode.workspace.getConfiguration('emmet').get<boolean>('useInlineCompletions');
const includedLanguages = getMappingForIncludedLanguages();
Object.keys(includedLanguages).forEach(language => {
if (languageMappingForCompletionProviders.has(language) && languageMappingForCompletionProviders.get(language) === includedLanguages[language]) {
return;
}
const mapping = includedLanguages[language];

if (useInlineCompletionProvider) {
const inlineCompletionsProvider = vscode.languages.registerInlineCompletionItemProvider({ language, scheme: '*' }, inlineCompletionProvider);
completionProviderDisposables.push(inlineCompletionsProvider);
}

const explicitProvider = vscode.languages.registerCompletionItemProvider({ language, scheme: '*' }, completionProvider, ...LANGUAGE_MODES[includedLanguages[language]]);
// Handle both single string and array of languages
let triggerChars: string[];
if (typeof mapping === 'string') {
triggerChars = LANGUAGE_MODES[mapping] || [];
} else if (Array.isArray(mapping)) {
triggerChars = mergeTriggerCharacters(mapping);
} else {
triggerChars = [];
}

const explicitProvider = vscode.languages.registerCompletionItemProvider({ language, scheme: '*' }, completionProvider, ...triggerChars);
completionProviderDisposables.push(explicitProvider);

languageMappingForCompletionProviders.set(language, includedLanguages[language]);
languageMappingForCompletionProviders.set(language, mapping);
});

Object.keys(LANGUAGE_MODES).forEach(language => {
Expand Down
41 changes: 41 additions & 0 deletions extensions/emmet/src/test/abbreviationAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,47 @@ suite('Tests for Expand Abbreviations (HTML)', () => {
await workspace.getConfiguration('emmet').update('excludeLanguages', oldConfig, ConfigurationTarget.Global);
});

test('Expand html when inside script tag with javascript type if js is mapped to array with html (HTML)', async () => {
const oldConfig = workspace.getConfiguration('emmet').inspect('includeLanguages')?.globalValue;
await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': ['html', 'css'] }, ConfigurationTarget.Global);
await withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(24, 10, 24, 10);
const expandPromise = expandEmmetAbbreviation(null);
if (!expandPromise) {
return Promise.resolve();
}
await expandPromise;
assert.strictEqual(editor.document.getText(), htmlContents.replace('span.bye', '<span class="bye"></span>'));
});
await workspace.getConfiguration('emmet').update('includeLanguages', oldConfig, ConfigurationTarget.Global);
});
Comment thread
Genius740Code marked this conversation as resolved.

test('Expand html in completion list when inside script tag with javascript type if js is mapped to array with html (HTML)', async () => {
const abbreviation = 'span.bye';
const expandedText = '<span class="bye"></span>';
const oldConfig = workspace.getConfiguration('emmet').inspect('includeLanguages')?.globalValue;
await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': ['html', 'css'] }, ConfigurationTarget.Global);
await withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => {
editor.selection = new Selection(24, 10, 24, 10);
const cancelSrc = new CancellationTokenSource();
const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, invokeCompletionContext);
if (!completionPromise) {
assert.strictEqual(1, 2, `Problem with expanding span.bye`);
return Promise.resolve();
}
const completionList = await completionPromise;
if (!completionList || !completionList.items || !completionList.items.length) {
assert.strictEqual(1, 2, `Problem with expanding span.bye`);
return Promise.resolve();
}
const emmetCompletionItem = completionList.items[0];
assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item (${emmetCompletionItem.label}) doesnt match.`);
assert.strictEqual(((<string>emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`);
return Promise.resolve();
});
await workspace.getConfiguration('emmet').update('includeLanguages', oldConfig, ConfigurationTarget.Global);
});

// test('No expanding when php (mapped syntax) is excluded in the settings', () => {
// return workspace.getConfiguration('emmet').update('excludeLanguages', ['php'], ConfigurationTarget.Global).then(() => {
// return testExpandAbbreviation('php', new Selection(9, 6, 9, 6), '', '', true).then(() => {
Expand Down
76 changes: 75 additions & 1 deletion extensions/emmet/src/test/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as assert from 'assert';
import 'mocha';
import { CancellationTokenSource, CompletionTriggerKind, Selection } from 'vscode';
import { CancellationTokenSource, CompletionTriggerKind, Selection, workspace, ConfigurationTarget } from 'vscode';
import { DefaultCompletionItemProvider } from '../defaultCompletionProvider';
import { closeAllEditors, withRandomFileEditor } from './testUtils';

Expand Down Expand Up @@ -86,6 +86,13 @@ interface TestCompletionItem {
documentation?: string;
}

/**
* Tests the completion provider for a given file extension and content
* @param fileExtension The file extension to test
* @param contents The file content with cursor position marked by '|'
* @param expectedItems Expected completion items or undefined if no completions expected
* @returns A promise that resolves when the test is complete
*/
function testCompletionProvider(fileExtension: string, contents: string, expectedItems: TestCompletionItem[] | undefined): Thenable<boolean> {
const cursorPos = contents.indexOf('|');
const slicedContents = contents.slice(0, cursorPos) + contents.slice(cursorPos + 1);
Expand Down Expand Up @@ -130,3 +137,70 @@ function testCompletionProvider(fileExtension: string, contents: string, expecte
return Promise.resolve();
});
}

suite('Tests for one-to-many language mapping', () => {
teardown(closeAllEditors);

test('Array mapping with HTML and CSS should provide HTML completions', async () => {
const oldConfig = workspace.getConfiguration('emmet').inspect('includeLanguages')?.globalValue;
await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': ['html', 'css'] }, ConfigurationTarget.Global);

try {
await testCompletionProvider('javascript', '<div |', [
{ label: 'div', documentation: `<div>|</div>` }
]);
} finally {
await workspace.getConfiguration('emmet').update('includeLanguages', oldConfig, ConfigurationTarget.Global);
}
});

test('Array mapping with HTML and CSS should provide CSS completions in style context', async () => {
const oldConfig = workspace.getConfiguration('emmet').inspect('includeLanguages')?.globalValue;
await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': ['html', 'css'] }, ConfigurationTarget.Global);

try {
await testCompletionProvider('javascript', '<div style="p|">', [
{ label: 'padding: ;', documentation: `padding: |;` }
]);
} finally {
await workspace.getConfiguration('emmet').update('includeLanguages', oldConfig, ConfigurationTarget.Global);
}
});

test('Array mapping with invalid languages should filter to valid ones', async () => {
const oldConfig = workspace.getConfiguration('emmet').inspect('includeLanguages')?.globalValue;
await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': ['html', 'invalidlang', 'css'] }, ConfigurationTarget.Global);

try {
await testCompletionProvider('javascript', '<div |', [
{ label: 'div', documentation: `<div>|</div>` }
]);
} finally {
await workspace.getConfiguration('emmet').update('includeLanguages', oldConfig, ConfigurationTarget.Global);
}
});

test('Backward compatibility: single string mapping still works', async () => {
const oldConfig = workspace.getConfiguration('emmet').inspect('includeLanguages')?.globalValue;
await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': 'html' }, ConfigurationTarget.Global);

try {
await testCompletionProvider('javascript', '<div |', [
{ label: 'div', documentation: `<div>|</div>` }
]);
} finally {
await workspace.getConfiguration('emmet').update('includeLanguages', oldConfig, ConfigurationTarget.Global);
}
});

test('Empty array should not register any completions', async () => {
const oldConfig = workspace.getConfiguration('emmet').inspect('includeLanguages')?.globalValue;
await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': [] }, ConfigurationTarget.Global);

try {
await testCompletionProvider('javascript', '<div |', undefined);
} finally {
await workspace.getConfiguration('emmet').update('includeLanguages', oldConfig, ConfigurationTarget.Global);
}
});
});
Loading