From 16caa86ed4eeff20b96b9c9abeb08307c46f34c3 Mon Sep 17 00:00:00 2001 From: ecmel Date: Sat, 9 Jan 2021 10:30:35 +0300 Subject: [PATCH] Removed validation --- CHANGELOG.md | 4 +- README.md | 17 +---- package.json | 22 +----- src/completion.ts | 112 ++++++------------------------ src/extension.ts | 31 +-------- src/test/suite/completion.test.ts | 99 ++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 157 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9fc832..94c5d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ All notable changes to the extension will be documented in this file. -## [1.6.4] - +## [1.7.0] - 2021-01-09 -- Use nextTick() +- Removed validation. ## [1.6.3] - 2021-01-07 diff --git a/README.md b/README.md index c053993..7031555 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Visual Studio Code CSS Intellisense for HTML -HTML `id` and `class` attribute completion and validation for Visual Studio Code. +HTML `id` and `class` attribute completion for Visual Studio Code. ## Features -- HTML `id` and `class` attribute completion and validation. +- HTML `id` and `class` attribute completion. - Supports linked and embedded style sheets. - Supports template inheritance. - Supports additional style sheets. @@ -145,19 +145,6 @@ If it is not possible to specify local or remote styles in HTML or via template This configuration is same as the [first](#linked-and-embedded-style-sheets) example. All relative paths will be evaluated relative to the HTML file being edited. -## Selector Validation - -Validated selectors can be configured with the `css.validation` setting. By default `class` selectors are validated: - -```json -{ - "css.validation": { - "id": false, - "class": true - } -} -``` - ## Supported Languages Supported languages can be configured with the `css.enabledLanguages` setting. By default `html` is enabled: diff --git a/package.json b/package.json index 8025947..ac8b890 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-html-css", "displayName": "HTML CSS Support", "description": "CSS Intellisense for HTML", - "version": "1.6.4", + "version": "1.7.0", "publisher": "ecmel", "license": "MIT", "homepage": "https://github.com/ecmel/vscode-html-css", @@ -42,29 +42,11 @@ "css.enabledLanguages": { "type": "array", "scope": "application", - "description": "List of languages where suggestions are desired.", + "description": "List of languages which suggestions are desired.", "default": [ "html" ] }, - "css.validation": { - "type": "object", - "scope": "resource", - "description": "Map of selectors to validate.", - "default": {}, - "properties": { - "id": { - "type": "boolean", - "description": "Whether to validate 'id' selectors.", - "default": false - }, - "class": { - "type": "boolean", - "description": "Whether to validate 'class' selectors.", - "default": true - } - } - }, "css.styleSheets": { "type": "array", "scope": "resource", diff --git a/src/completion.ts b/src/completion.ts index f973028..8f7b411 100644 --- a/src/completion.ts +++ b/src/completion.ts @@ -9,10 +9,7 @@ import { CompletionItemKind, CompletionItemProvider, CompletionList, - Diagnostic, - DiagnosticSeverity, Disposable, - languages, Position, ProviderResult, Range, @@ -21,11 +18,6 @@ import { workspace } from "vscode"; -export type Validation = { - id: boolean, - class: boolean -}; - export type Selector = { ids: Map, classes: Map, @@ -38,19 +30,16 @@ export class SelectorCompletionItemProvider implements CompletionItemProvider, D readonly start = new Position(0, 0); readonly cache = new Map>(); readonly watchers = new Map(); - readonly selectors = new Map(); - readonly warnings = languages.createDiagnosticCollection(); readonly isRemote = /^https?:\/\//i; + readonly canComplete = /(id|class|className)\s*=\s*("|')(?:(?!\2).)*$/si; readonly findLinkRel = /rel\s*=\s*("|')((?:(?!\1).)+)\1/si; readonly findLinkHref = /href\s*=\s*("|')((?:(?!\1).)+)\1/si; readonly findExtended = /(?:{{<|{{>|{%\s*extends|@extends\s*\()\s*("|')?([./A-Za-z_0-9\\\-]+)\1\s*(?:\)|%}|}})/i; dispose() { this.watchers.forEach(v => v.dispose()); - this.cache.clear(); this.watchers.clear(); - this.selectors.clear(); - this.warnings.dispose(); + this.cache.clear(); } watchFile(uri: Uri, listener: (e: Uri) => any) { @@ -71,15 +60,6 @@ export class SelectorCompletionItemProvider implements CompletionItemProvider, D return workspace.getConfiguration("css", uri).get("styleSheets", []); } - getValidation(uri: Uri): Validation { - const config = workspace.getConfiguration("css", uri); - - return { - id: config.get("validation.id", false), - class: config.get("validation.class", true) - }; - } - getRelativePath(uri: Uri, spec: string, ext?: string): string { const folder = workspace.getWorkspaceFolder(uri); const name = ext ? join(dirname(spec), basename(spec, ext) + ext) : spec; @@ -214,100 +194,52 @@ export class SelectorCompletionItemProvider implements CompletionItemProvider, D } } - async validate(document: TextDocument): Promise { + async findAll(document: TextDocument, kind: CompletionItemKind): Promise> { const keys = new Set(); const uri = document.uri; const text = document.getText(); this.findDocumentStyles(uri, keys, text); + await this.findStyleSheets(uri, keys); await this.findDocumentLinks(uri, keys, text); await this.findExtendedStyles(uri, keys, text); - const ids = new Map(); - const classes = new Map(); - const rangesId: Range[] = []; - const rangesClass: Range[] = []; - - keys.forEach(key => this.cache.get(key)?.forEach((v, k) => - (v.kind === CompletionItemKind.Value ? ids : classes).set(k, v))); - - const validation = this.getValidation(uri); - const diagnostics: Diagnostic[] = []; - const findAttribute = /(id|class|className)\s*=\s*("|')(.*?)\2/gsi; - - let attribute; - - while ((attribute = findAttribute.exec(text)) !== null) { - const offset = findAttribute.lastIndex - - attribute[3].length - + attribute[3].indexOf(attribute[2]); - - (attribute[1] === "id" ? rangesId : rangesClass).push(new Range( - document.positionAt(offset), - document.positionAt(findAttribute.lastIndex - 1))); - - const findSelector = /([^(\[{}\])\s]+)(?![^(\[{]*[}\])])/gi; - - let value; - - while ((value = findSelector.exec(attribute[3])) !== null) { - const anchor = findSelector.lastIndex + offset; - const end = document.positionAt(anchor); - const start = document.positionAt(anchor - value[1].length); + const items = new Map(); - if (attribute[1] === "id") { - if (validation.id && !ids.has(value[1])) { - diagnostics.push(new Diagnostic(new Range(start, end), - `CSS id selector '${value[1]}' not found.`, - DiagnosticSeverity.Information)); - } - } else { - if (validation.class && !classes.has(value[1])) { - diagnostics.push(new Diagnostic(new Range(start, end), - `CSS class selector '${value[1]}' not found.`, - DiagnosticSeverity.Warning)); - } - } + keys.forEach(key => this.cache.get(key)?.forEach((v, k) => { + if (v.kind === kind) { + items.set(k, v); } - } + })); - this.warnings.set(uri, diagnostics); - this.selectors.set(uri.toString(), { ids, classes, rangesId, rangesClass }); + return items; } provideCompletionItems( document: TextDocument, position: Position, token: CancellationToken, - context: CompletionContext) - : ProviderResult> { + context: CompletionContext): ProviderResult> { return new Promise((resolve, reject) => nextTick(() => { if (token.isCancellationRequested) { reject(); - return; - } - - const selector = this.selectors.get(document.uri.toString()); + } else { + const range = new Range(this.start, position); + const text = document.getText(range); + const canComplete = this.canComplete.exec(text); - if (selector) { - for (const range of selector.rangesClass) { - if (range.contains(position)) { - resolve([...selector.classes.values()]); - return; - } - } + if (canComplete) { + const kind = canComplete[1] === "id" + ? CompletionItemKind.Value + : CompletionItemKind.Enum; - for (const range of selector.rangesId) { - if (range.contains(position)) { - resolve([...selector.ids.values()]); - return; - } + this.findAll(document, kind).then((items => resolve([...items.values()]))); + } else { + reject(); } } - - reject(); })); } } diff --git a/src/extension.ts b/src/extension.ts index 9766b19..b79ba44 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,41 +1,12 @@ import { SelectorCompletionItemProvider } from "./completion"; -import { - ExtensionContext, - languages, - TextDocument, - TextDocumentChangeEvent, - workspace -} from "vscode"; +import { ExtensionContext, languages, workspace } from "vscode"; export function activate(context: ExtensionContext) { const config = workspace.getConfiguration("css"); const enabledLanguages = config.get("enabledLanguages", ["html"]); - const timeouts = new Map(); const provider = new SelectorCompletionItemProvider(); - const validate = (e: TextDocumentChangeEvent | TextDocument) => { - const document = (e as TextDocumentChangeEvent).document || e; - - if (enabledLanguages.includes(document.languageId)) { - const uri = document.uri.toString(); - const timeout = timeouts.get(uri); - - if (timeout) { - clearTimeout(timeout); - } - - timeouts.set(uri, setTimeout(() => { - timeouts.delete(uri); - provider.validate(document); - }, 300)); - } - }; - - workspace.textDocuments.forEach(validate); - context.subscriptions.push( - workspace.onDidOpenTextDocument(validate), - workspace.onDidChangeTextDocument(validate), languages.registerCompletionItemProvider(enabledLanguages, provider), provider ); diff --git a/src/test/suite/completion.test.ts b/src/test/suite/completion.test.ts index f5b2f0f..a22bda1 100644 --- a/src/test/suite/completion.test.ts +++ b/src/test/suite/completion.test.ts @@ -16,6 +16,41 @@ suite("SelectorCompletionItemProvider Test Suite", () => { assert.strictEqual(provider.isRemote.test("https://example.com/example.css"), true); }); + test("RegEx: canComplete", () => { + const provider = new SelectorCompletionItemProvider(); + + assert.strictEqual(provider.canComplete.test(""), false); + assert.strictEqual(provider.canComplete.test("class=\""), true); + assert.strictEqual(provider.canComplete.test("class=\"\""), false); + assert.strictEqual(provider.canComplete.test("class = \""), true); + assert.strictEqual(provider.canComplete.test("class = \"\""), false); + + assert.strictEqual(provider.canComplete.test(` + class = "someClass + `), true); + + assert.strictEqual(provider.canComplete.test(` + class + = "someClass + `), true); + assert.strictEqual(provider.canComplete.test(` + class = + "someClass + + `), true); + assert.strictEqual(provider.canComplete.test(` + class = + "someClass + + "`), false); + assert.strictEqual(provider.canComplete.test(` + class = "some" + class = + "someClass + + "`), false); + }); + test("RegEx: findLinkRel", () => { const provider = new SelectorCompletionItemProvider(); @@ -63,4 +98,68 @@ suite("SelectorCompletionItemProvider Test Suite", () => { @extends('base') `)?.[2], "base"); }); + + test("Rejects outside class attribute", (done) => { + const provider = new SelectorCompletionItemProvider(); + const document = new MockDocument(""); + + const result = provider.provideCompletionItems( + document, + position, + token, + context) as Thenable; + + result.then(items => done(new Error("Should reject!")), () => done()); + }); + + test("Completes from style tag", async () => { + const provider = new SelectorCompletionItemProvider(); + const document = new MockDocument("); + + assert.strictEqual(items.length, 1); + }); + + test("Completes from link tag", async () => { + const provider = new SelectorCompletionItemProvider(); + const document = new MockDocument(` + + { + const provider = new class extends SelectorCompletionItemProvider { + getStyleSheets(uri: Uri): string[] { + return [ + "https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" + ]; + } + }(); + + const document = new MockDocument("); + + assert.notStrictEqual(items.length, 0); + }); });