diff --git a/fontes/ativacao-linguagens.ts b/fontes/ativacao-linguagens.ts index 5e6a2af..597652b 100644 --- a/fontes/ativacao-linguagens.ts +++ b/fontes/ativacao-linguagens.ts @@ -76,7 +76,8 @@ async function ativarFoles(context: vscode.ExtensionContext): Promise { { scheme: 'file', language: 'foles' }, { scheme: 'untitled', language: 'foles' } ], - new FolesProvedorCompletude() + new FolesProvedorCompletude(), + ':' ), vscode.languages.registerHoverProvider( [ diff --git a/fontes/completude/foles-provedor-completude.ts b/fontes/completude/foles-provedor-completude.ts index 87979d1..28a922e 100644 --- a/fontes/completude/foles-provedor-completude.ts +++ b/fontes/completude/foles-provedor-completude.ts @@ -1,49 +1,288 @@ import * as vscode from 'vscode'; import listaModificadores from '@designliquido/foles/extensao/lista-modificadores'; +import * as modificadoresFolEs from '@designliquido/foles/modificadores'; +import { cores } from '@designliquido/foles/modificadores/atributos/cores'; +import { valoresGlobais } from '@designliquido/foles/modificadores/atributos/globais'; +import { Cores } from '@designliquido/foles/listas/cores'; import { DicionarioEstruturasLmht } from '@designliquido/foles/estruturas/dicionario-estruturas-lmht'; +import { DicionarioValores } from '@designliquido/foles/valores/dicionario-valores'; -export class FolesProvedorCompletude implements vscode.CompletionItemProvider { +type DicionarioTexto = Record; + +type ConstrutorModificador = { + new (valores: unknown[], pragmas?: unknown, variavel?: boolean): { + valoresAceitos?: DicionarioTexto; + }; + nomeCss?: string; + nomeFolEs?: string | string[]; +}; + +interface ContextoDeclaracao { + nomeModificador?: string; + possuiDoisPontos: boolean; +} + +interface ValorCompletude { + nomeFolEs: string; + nomeCss?: string; + tipo: vscode.CompletionItemKind; + origem: string; +} + +const METODOS_COR = new Set(['hex', 'hsl', 'hsla', 'rgb', 'rgba']); +const MODIFICADORES_COR = new Set(Cores); +const MODIFICADORES = listaModificadores as Record; + +function nomesFolEs(construtor: ConstrutorModificador): string[] { + if (!construtor.nomeFolEs) { + return []; + } + + return Array.isArray(construtor.nomeFolEs) + ? construtor.nomeFolEs + : [construtor.nomeFolEs]; +} + +function construirValoresPorModificador(): Map { + const valoresPorNome = new Map(); + const valoresPorNomeCss = new Map(); + + for (const exportacao of Object.values(modificadoresFolEs)) { + if (typeof exportacao !== 'function') { + continue; + } + + const construtor = exportacao as ConstrutorModificador; + const nomes = nomesFolEs(construtor); + + if (nomes.length === 0) { + continue; + } + + try { + const instancia = new construtor([], undefined, true); + const valoresAceitos = instancia.valoresAceitos; + if (!valoresAceitos || typeof valoresAceitos !== 'object') { + continue; + } + + for (const nome of nomes) { + valoresPorNome.set(nome, valoresAceitos); + } + + if (construtor.nomeCss) { + valoresPorNomeCss.set(construtor.nomeCss, valoresAceitos); + } + } catch { + // Alguns modificadores podem exigir estado adicional em versões futuras + // do pacote. Nesse caso, a completude mantém os valores globais e os + // dicionários especializados, sem interromper o provedor. + } + } + + for (const [nome, metadados] of Object.entries(MODIFICADORES)) { + if (valoresPorNome.has(nome)) { + continue; + } + + const valoresPeloNomeCss = valoresPorNomeCss.get(metadados.nomeCss); + if (valoresPeloNomeCss) { + valoresPorNome.set(nome, valoresPeloNomeCss); + } + } + + return valoresPorNome; +} + +const VALORES_POR_MODIFICADOR = construirValoresPorModificador(); + +export class FolesProvedorCompletude implements vscode.CompletionItemProvider { private profundidadeNaCursorPosition(document: vscode.TextDocument, position: vscode.Position): number { const textoCompleto = document.getText(); const linhas = textoCompleto.split('\n'); let offset = 0; + for (let i = 0; i < position.line; i++) { offset += linhas[i].length + 1; } + offset += position.character; const trecho = textoCompleto.substring(0, offset); let profundidade = 0; + for (const char of trecho) { if (char === '{') profundidade++; else if (char === '}') profundidade--; } + return profundidade; } + private contextoDeclaracaoAtual(document: vscode.TextDocument, position: vscode.Position): ContextoDeclaracao { + const textoLinha = document.lineAt(position.line).text.substring(0, position.character); + const trechoAtual = textoLinha.split(/[;{}]/).pop() ?? ''; + const correspondencia = trechoAtual.match(/^\s*([\p{L}\p{N}_-]+)\s*:\s*[^;{}]*$/u); + + if (!correspondencia) { + return { possuiDoisPontos: false }; + } + + return { + nomeModificador: correspondencia[1], + possuiDoisPontos: true, + }; + } + private itensSeletoresLmht(): vscode.CompletionItem[] { const itens: vscode.CompletionItem[] = []; + for (const [chave, Classe] of Object.entries(DicionarioEstruturasLmht)) { const item = new vscode.CompletionItem(chave, vscode.CompletionItemKind.Interface); const instancia = new (Classe as any)(); item.documentation = `Equivalente em HTML: <${instancia.tagHtml}>`; itens.push(item); } + return itens; } - provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): vscode.ProviderResult | vscode.CompletionItem[]> - { - const itens: vscode.CompletionItem[] = this.itensSeletoresLmht(); + private itensModificadores(): vscode.CompletionItem[] { + const itens: vscode.CompletionItem[] = []; - if (this.profundidadeNaCursorPosition(document, position) > 0) { - for (const [chave, valor] of Object.entries(listaModificadores)) { - const item = new vscode.CompletionItem(chave, vscode.CompletionItemKind.Property); - item.documentation = `Equivalente em CSS: ${valor.nomeCss}`; - itens.push(item); - } + for (const [chave, valor] of Object.entries(MODIFICADORES)) { + const item = new vscode.CompletionItem(chave, vscode.CompletionItemKind.Property); + item.documentation = valor.descricao + ? `${valor.descricao}\n\nEquivalente em CSS: ${valor.nomeCss}` + : `Equivalente em CSS: ${valor.nomeCss}`; + item.detail = `CSS: ${valor.nomeCss}`; + itens.push(item); } return itens; } -} \ No newline at end of file + + private modificadorAceitaCor(nomeModificador: string): boolean { + if (MODIFICADORES_COR.has(nomeModificador)) { + return true; + } + + const nomeCss = MODIFICADORES[nomeModificador]?.nomeCss; + return typeof nomeCss === 'string' && nomeCss.includes('color'); + } + + private adicionarValor( + valores: Map, + nomeFolEs: string, + nomeCss: string | undefined, + tipo: vscode.CompletionItemKind, + origem: string + ): void { + const existente = valores.get(nomeFolEs); + + if (existente && existente.tipo === vscode.CompletionItemKind.Color) { + return; + } + + valores.set(nomeFolEs, { + nomeFolEs, + nomeCss, + tipo, + origem, + }); + } + + private valoresDoModificador(nomeModificador: string): ValorCompletude[] { + if (!MODIFICADORES[nomeModificador]) { + return []; + } + + const valores = new Map(); + const valoresEspecificos = VALORES_POR_MODIFICADOR.get(nomeModificador) ?? {}; + + for (const [nomeFolEs, nomeCss] of Object.entries(valoresEspecificos)) { + this.adicionarValor( + valores, + nomeFolEs, + nomeCss, + vscode.CompletionItemKind.Value, + 'Valor aceito pelo modificador' + ); + } + + for (const [nomeFolEs, nomeCss] of Object.entries(valoresGlobais)) { + this.adicionarValor( + valores, + nomeFolEs, + nomeCss, + vscode.CompletionItemKind.Value, + 'Valor global' + ); + } + + if (this.modificadorAceitaCor(nomeModificador)) { + for (const [nomeFolEs, nomeCss] of Object.entries(cores)) { + this.adicionarValor( + valores, + nomeFolEs, + nomeCss, + vscode.CompletionItemKind.Color, + 'Cor' + ); + } + + for (const nomeMetodo of METODOS_COR) { + if (DicionarioValores[nomeMetodo]) { + this.adicionarValor( + valores, + nomeMetodo, + undefined, + vscode.CompletionItemKind.Function, + 'Função de cor' + ); + } + } + } + + return Array.from(valores.values()).sort((a, b) => a.nomeFolEs.localeCompare(b.nomeFolEs)); + } + + private itensValores(nomeModificador: string): vscode.CompletionItem[] { + return this.valoresDoModificador(nomeModificador).map(valor => { + const item = new vscode.CompletionItem(valor.nomeFolEs, valor.tipo); + item.insertText = valor.nomeFolEs; + item.detail = valor.nomeCss + ? `Equivalente em CSS: ${valor.nomeCss}` + : valor.origem; + item.documentation = valor.nomeCss + ? `${valor.origem}. Equivalente em CSS: ${valor.nomeCss}.` + : valor.origem; + item.sortText = `${valor.tipo === vscode.CompletionItemKind.Color ? '0' : '1'}-${valor.nomeFolEs}`; + return item; + }); + } + + provideCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken, + context: vscode.CompletionContext + ): vscode.ProviderResult | vscode.CompletionItem[]> { + const profundidade = this.profundidadeNaCursorPosition(document, position); + + if (profundidade <= 0) { + return this.itensSeletoresLmht(); + } + + const contextoDeclaracao = this.contextoDeclaracaoAtual(document, position); + + if (contextoDeclaracao.possuiDoisPontos && contextoDeclaracao.nomeModificador) { + return this.itensValores(contextoDeclaracao.nomeModificador); + } + + return this.itensModificadores(); + } +} diff --git a/testes/completude/foles-provedor-completude.test.ts b/testes/completude/foles-provedor-completude.test.ts index c632919..de9f11b 100644 --- a/testes/completude/foles-provedor-completude.test.ts +++ b/testes/completude/foles-provedor-completude.test.ts @@ -1,5 +1,7 @@ // @ts-nocheck import { describe, it, expect, jest, beforeEach } from '@jest/globals'; +import * as fs from 'fs'; +import * as path from 'path'; import * as vscode from 'vscode'; jest.mock('vscode', () => ({ @@ -7,24 +9,99 @@ jest.mock('vscode', () => ({ Property: 9, Function: 2, Interface: 7, + Value: 12, + Color: 15, }, CompletionItem: class CompletionItem { constructor(public label: string, public kind?: number) {} documentation: any; + detail: any; + insertText: any; + sortText: any; }, }), { virtual: true }); jest.mock('@designliquido/foles/extensao/lista-modificadores', () => ({ __esModule: true, default: { - 'alinhamento': { nomeCss: 'text-align' }, - 'cor': { nomeCss: 'color' }, - 'fundo': { nomeCss: 'background' }, - 'margem': { nomeCss: 'margin' }, - 'padding': { nomeCss: 'padding' }, + 'alinhamento': { + nomeCss: 'text-align', + descricao: 'Define o alinhamento do texto.', + }, + 'cor': { + nomeCss: 'color', + descricao: 'Define a cor do texto.', + }, + 'fundo': { + nomeCss: 'background', + descricao: 'Define o fundo.', + }, + 'margem': { + nomeCss: 'margin', + descricao: 'Define a margem.', + }, + 'recuo': { + nomeCss: 'padding', + descricao: 'Define o recuo interno.', + }, } })); +jest.mock('@designliquido/foles/modificadores', () => ({ + Alinhamento: class Alinhamento { + static nomeFolEs = 'alinhamento'; + static nomeCss = 'text-align'; + valoresAceitos = { + esquerda: 'left', + centro: 'center', + direita: 'right', + }; + }, + Fundo: class Fundo { + static nomeFolEs = 'fundo'; + static nomeCss = 'background'; + valoresAceitos = { + fixo: 'fixed', + repetir: 'repeat', + }; + }, + Margem: class Margem { + static nomeFolEs = 'margem'; + static nomeCss = 'margin'; + valoresAceitos = { + auto: 'auto', + }; + }, +})); + +jest.mock('@designliquido/foles/modificadores/atributos/cores', () => ({ + cores: { + azul: 'blue', + vermelho: 'red', + }, +})); + +jest.mock('@designliquido/foles/modificadores/atributos/globais', () => ({ + valoresGlobais: { + herdar: 'inherit', + inicial: 'initial', + }, +})); + +jest.mock('@designliquido/foles/listas/cores', () => ({ + Cores: ['cor'], +})); + +jest.mock('@designliquido/foles/valores/dicionario-valores', () => ({ + DicionarioValores: { + calcular: class {}, + hsl: class {}, + hsla: class {}, + rgb: class {}, + rgba: class {}, + }, +})); + jest.mock('@designliquido/foles/estruturas/dicionario-estruturas-lmht', () => ({ DicionarioEstruturasLmht: { 'divisao': class { tagHtml = 'div'; }, @@ -38,8 +115,8 @@ import { FolesProvedorCompletude } from '../../fontes/completude/foles-provedor- function criarDocumento(linhas: string[] = ['']): any { return { lineAt: jest.fn((linha: any) => { - const idx = typeof linha === 'number' ? linha : linha.line ?? 0; - return { text: linhas[idx] ?? '' }; + const indice = typeof linha === 'number' ? linha : linha.line ?? 0; + return { text: linhas[indice] ?? '' }; }), getText: jest.fn(() => linhas.join('\n')), }; @@ -49,10 +126,21 @@ function criarPosicao(line = 0, character = 0): any { return { line, character }; } +function completar(provedor: any, linhas: string[], linha: number, caractere: number): any[] { + return provedor.provideCompletionItems( + criarDocumento(linhas), + criarPosicao(linha, caractere), + { isCancellationRequested: false }, + {} + ); +} + +function rotulos(itens: any[]): string[] { + return itens.map(item => item.label); +} + describe('FolesProvedorCompletude', () => { let provedor: any; - const mockToken: any = { isCancellationRequested: false }; - const mockContext: any = {}; beforeEach(() => { jest.clearAllMocks(); @@ -64,130 +152,172 @@ describe('FolesProvedorCompletude', () => { expect(typeof provedor.provideCompletionItems).toBe('function'); }); - describe('nível raiz (fora de bloco)', () => { - it('retorna apenas seletores LMHT na raiz', () => { - const doc = criarDocumento(['']); - const pos = criarPosicao(0, 0); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(Array.isArray(items)).toBe(true); - expect(items.length).toBe(3); // 3 tags LMHT mockadas + describe('nível raiz', () => { + it('retorna somente seletores LMHT fora de blocos', () => { + const itens = completar(provedor, [''], 0, 0); + + expect(itens).toHaveLength(3); + expect(itens.every(item => item.kind === vscode.CompletionItemKind.Interface)).toBe(true); + expect(rotulos(itens)).toEqual(['divisao', 'paragrafo', 'lmht']); + }); + + it('retorna seletores novamente depois do fechamento do bloco', () => { + const linhas = ['.classe {', ' cor: vermelho;', '}', '']; + const itens = completar(provedor, linhas, 3, 0); + + expect(itens).toHaveLength(3); + expect(itens.every(item => item.kind === vscode.CompletionItemKind.Interface)).toBe(true); + }); + + it('documenta o equivalente HTML do seletor', () => { + const itens = completar(provedor, [''], 0, 0); + const divisao = itens.find(item => item.label === 'divisao'); + + expect(divisao.documentation).toBe('Equivalente em HTML:
'); + }); + }); + + describe('dentro de bloco antes dos dois-pontos', () => { + it('retorna somente modificadores e não inclui seletores', () => { + const itens = completar(provedor, ['.classe {', ' ', '}'], 1, 4); + + expect(itens).toHaveLength(5); + expect(itens.every(item => item.kind === vscode.CompletionItemKind.Property)).toBe(true); + expect(itens.some(item => item.kind === vscode.CompletionItemKind.Interface)).toBe(false); }); - it('seletores LMHT são do tipo Interface', () => { - const doc = criarDocumento(['']); - const pos = criarPosicao(0, 0); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - items.forEach((item: any) => { - expect(item.kind).toBe(vscode.CompletionItemKind.Interface); - }); + it('inclui os modificadores disponíveis', () => { + const itens = completar(provedor, ['.classe {', ' ', '}'], 1, 4); + + expect(rotulos(itens)).toEqual([ + 'alinhamento', + 'cor', + 'fundo', + 'margem', + 'recuo', + ]); + }); + + it('inclui descrição e equivalente CSS do modificador', () => { + const itens = completar(provedor, ['.classe {', ' ', '}'], 1, 4); + const cor = itens.find(item => item.label === 'cor'); + + expect(cor.documentation).toContain('Define a cor do texto.'); + expect(cor.documentation).toContain('Equivalente em CSS: color'); + expect(cor.detail).toBe('CSS: color'); + }); + + it('volta a sugerir modificadores depois de ponto e vírgula', () => { + const linha = ' cor: vermelho; '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + + expect(itens).toHaveLength(5); + expect(itens.every(item => item.kind === vscode.CompletionItemKind.Property)).toBe(true); + }); + }); + + describe('após o nome do modificador', () => { + it('sugere valores específicos do modificador', () => { + const linha = ' alinhamento: '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + + expect(rotulos(itens)).toEqual(expect.arrayContaining([ + 'centro', + 'direita', + 'esquerda', + ])); + expect(itens.find(item => item.label === 'centro').detail) + .toBe('Equivalente em CSS: center'); }); - it('inclui seletor divisao na raiz', () => { - const doc = criarDocumento(['']); - const pos = criarPosicao(0, 0); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.label === 'divisao')).toBe(true); + it('não inclui seletores nem modificadores ao completar valores', () => { + const linha = ' alinhamento: '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + + expect(itens.some(item => item.kind === vscode.CompletionItemKind.Interface)).toBe(false); + expect(itens.some(item => item.kind === vscode.CompletionItemKind.Property)).toBe(false); }); - it('inclui seletor lmht na raiz', () => { - const doc = criarDocumento(['']); - const pos = criarPosicao(0, 0); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.label === 'lmht')).toBe(true); + it('inclui valores globais válidos para qualquer modificador conhecido', () => { + const linha = ' margem: '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + + expect(rotulos(itens)).toEqual(expect.arrayContaining(['herdar', 'inicial'])); + expect(itens.find(item => item.label === 'herdar').kind) + .toBe(vscode.CompletionItemKind.Value); }); - it('não inclui modificadores na raiz', () => { - const doc = criarDocumento(['']); - const pos = criarPosicao(0, 0); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.kind === vscode.CompletionItemKind.Property)).toBe(false); + it('não sugere valores para modificador desconhecido', () => { + const linha = ' inexistente: '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + + expect(itens).toEqual([]); }); - it('retorna apenas seletores LMHT após fechar bloco', () => { - const doc = criarDocumento(['.classe {', ' cor: red;', '}', '']); - const pos = criarPosicao(3, 0); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.length).toBe(3); - expect(items.every((i: any) => i.kind === vscode.CompletionItemKind.Interface)).toBe(true); + it('reconhece declaração na mesma linha da abertura do bloco', () => { + const linha = '.classe { alinhamento: '; + const itens = completar(provedor, [linha], 0, linha.length); + + expect(rotulos(itens)).toEqual(expect.arrayContaining(['centro', 'esquerda'])); }); }); - describe('dentro de um bloco seletor', () => { - it('retorna seletores LMHT e modificadores', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.length).toBe(8); // 3 LMHT + 5 modificadores - }); - - it('inclui itens do tipo Interface (seletores LMHT)', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.kind === vscode.CompletionItemKind.Interface)).toBe(true); - }); - - it('inclui itens do tipo Property (modificadores)', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.kind === vscode.CompletionItemKind.Property)).toBe(true); - }); - - it('todos os itens têm label definido', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - items.forEach((item: any) => { - expect(item.label).toBeDefined(); - expect(typeof item.label).toBe('string'); - }); - }); - - it('todos os itens têm documentação', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - items.forEach((item: any) => { - expect(item.documentation).toBeDefined(); - }); - }); - - describe('modificadores mockados', () => { - it('inclui alinhamento', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.label === 'alinhamento')).toBe(true); - }); - - it('inclui cor', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.label === 'cor')).toBe(true); - }); - - it('inclui fundo', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.label === 'fundo')).toBe(true); - }); - - it('inclui margem', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.label === 'margem')).toBe(true); - }); - - it('inclui padding', () => { - const doc = criarDocumento(['.classe {', ' ', '}']); - const pos = criarPosicao(1, 4); - const items = provedor.provideCompletionItems(doc, pos, mockToken, mockContext); - expect(items.some((i: any) => i.label === 'padding')).toBe(true); - }); + describe('completude de cores', () => { + it('sugere nomes de cores após cor:', () => { + const linha = ' cor: '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + + expect(rotulos(itens)).toEqual(expect.arrayContaining(['azul', 'vermelho'])); }); + + it('marca cores com CompletionItemKind.Color para amostra visual', () => { + const linha = ' cor: '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + const vermelho = itens.find(item => item.label === 'vermelho'); + + expect(vermelho.kind).toBe(vscode.CompletionItemKind.Color); + expect(vermelho.detail).toBe('Equivalente em CSS: red'); + expect(vermelho.insertText).toBe('vermelho'); + }); + + it('sugere funções de cor existentes no dicionário de valores', () => { + const linha = ' cor: '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + + expect(rotulos(itens)).toEqual(expect.arrayContaining([ + 'hsl', + 'hsla', + 'rgb', + 'rgba', + ])); + expect(itens.find(item => item.label === 'rgb').kind) + .toBe(vscode.CompletionItemKind.Function); + }); + + it('não inclui função genérica que não é função de cor', () => { + const linha = ' cor: '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + + expect(rotulos(itens)).not.toContain('calcular'); + }); + + it('não duplica valores quando um valor específico também é uma cor', () => { + const linha = ' cor: '; + const itens = completar(provedor, ['.classe {', linha, '}'], 1, linha.length); + const vermelhos = itens.filter(item => item.label === 'vermelho'); + + expect(vermelhos).toHaveLength(1); + }); + }); + + it('registra dois-pontos como gatilho de completude de FolEs', () => { + const arquivoAtivacao = fs.readFileSync( + path.resolve(__dirname, '../../fontes/ativacao-linguagens.ts'), + 'utf-8' + ); + + expect(arquivoAtivacao).toMatch( + /new FolesProvedorCompletude\(\)\s*,\s*['"]:['"]/ + ); }); });