diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 0246dd5a..df9a9627 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -21,7 +21,7 @@ import { TextDocumentChangeEvent, } from "vscode-languageserver/node"; import { TextDocument } from "vscode-languageserver-textdocument"; -import { URI } from "vscode-uri"; +import { URI, Utils } from "vscode-uri"; import type { FileSystemProvider } from "./file-system"; import { getFileSystemProvider } from "./file-system-provider"; import { RuntimeEnvironment } from "./runtime"; @@ -34,6 +34,10 @@ import { getSassRegionsDocument } from "./embedded"; import WorkspaceScanner from "./workspace-scanner"; import { createLogger, type Logger } from "./logger"; import merge from "lodash.merge"; +import { + ICSSDataProvider, + newCSSDataProvider, +} from "@somesass/vscode-css-languageservice"; export class SomeSassServer { private readonly connection: Connection; @@ -171,61 +175,107 @@ export class SomeSassServer { return settings; }; + const applyCustomData = async ( + configuration: LanguageServerConfiguration, + ) => { + const paths: string[] = []; + if (configuration.css.customData) { + paths.push(...configuration.css.customData); + } + if (configuration.sass.customData) { + paths.push(...configuration.sass.customData); + } + if (configuration.scss.customData) { + paths.push(...configuration.scss.customData); + } + + const customDataProviders = await Promise.all( + paths.map(async (path) => { + try { + let uri = path.startsWith("/") + ? URI.parse(path) + : Utils.joinPath(workspaceRoot!, path); + + const content = await fileSystemProvider!.readFile(uri, "utf-8"); + const rawData = JSON.parse(content); + + return newCSSDataProvider({ + version: rawData.version || 1, + properties: rawData.properties || [], + atDirectives: rawData.atDirectives || [], + pseudoClasses: rawData.pseudoClasses || [], + pseudoElements: rawData.pseudoElements || [], + }); + } catch (error) { + this.log.debug(String(error)); + return newCSSDataProvider({ version: 1 }); + } + }), + ); + + ls!.setDataProviders(customDataProviders); + }; + this.connection.onInitialized(async () => { try { // Let other methods await the result of the initial scan before proceeding - initialScan = new Promise((resolve, reject) => { - const configurationRequests = [ - this.connection.workspace.getConfiguration("somesass"), - this.connection.workspace.getConfiguration("editor"), - ]; - - Promise.all(configurationRequests).then((configs) => { - if ( - !ls || - !clientCapabilities || - !workspaceRoot || - !fileSystemProvider - ) { - return reject( - new Error( - "Got onInitialized without onInitialize readying up all required globals", - ), - ); - } - - let [somesass, editor] = configs as [ - Partial, - Partial, + initialScan = new Promise( + (resolveInitialScan, rejectInitialScan) => { + const configurationRequests = [ + this.connection.workspace.getConfiguration("somesass"), + this.connection.workspace.getConfiguration("editor"), ]; - const configuration = applyConfiguration(somesass, editor); - - this.log.debug("Scanning workspace for files"); - - return fileSystemProvider - .findFiles( - "**/*.{css,scss,sass,svelte,astro,vue}", - configuration.workspace.exclude, - ) - .then((files) => { - this.log.debug(`Found ${files.length} files, starting parse`); - - workspaceScanner = new WorkspaceScanner( - ls!, - fileSystemProvider!, - ); - - return workspaceScanner.scan(files); + Promise.all(configurationRequests) + .then((configs) => { + if ( + !ls || + !clientCapabilities || + !workspaceRoot || + !fileSystemProvider + ) { + throw new Error( + "Got onInitialized without onInitialize readying up all required globals", + ); + } + + let [somesass, editor] = configs as [ + Partial, + Partial, + ]; + + const configuration = applyConfiguration(somesass, editor); + + return applyCustomData(configuration) + .then(() => + fileSystemProvider!.findFiles( + "**/*.{css,scss,sass,svelte,astro,vue}", + configuration.workspace.exclude, + ), + ) + .then((files) => { + this.log.debug( + `Found ${files.length} files, starting parse`, + ); + + workspaceScanner = new WorkspaceScanner( + ls!, + fileSystemProvider!, + ); + + return workspaceScanner.scan(files); + }) + .then((promises) => { + this.log.debug( + `Initial scan finished, parsed ${promises.length} files`, + ); + resolveInitialScan(); + }) + .catch((reason) => rejectInitialScan(reason)); }) - .then((promises) => { - this.log.debug( - `Initial scan finished, parsed ${promises.length} files`, - ); - resolve(); - }); - }); - }); + .catch((reason) => rejectInitialScan(reason)); + }, + ); await initialScan; } catch (error) { this.log.fatal(String(error)); diff --git a/packages/language-services/src/features/__tests__/do-diagnostics.test.ts b/packages/language-services/src/features/__tests__/do-diagnostics.test.ts new file mode 100644 index 00000000..5966ff4c --- /dev/null +++ b/packages/language-services/src/features/__tests__/do-diagnostics.test.ts @@ -0,0 +1,88 @@ +import { test, assert, beforeEach } from "vitest"; +import { + defaultConfiguration, + getLanguageService, +} from "../../language-services"; +import { DiagnosticSeverity } from "../../language-services-types"; +import { getOptions } from "../../utils/test-helpers"; + +const { fileSystemProvider, ...rest } = getOptions(); +const ls = getLanguageService({ fileSystemProvider, ...rest }); + +beforeEach(() => { + ls.clearCache(); + ls.configure(defaultConfiguration); +}); + +test("reports an unknown at-rule", async () => { + const document = fileSystemProvider.createDocument(` +@tailwind base; +`); + + const result = await ls.doDiagnostics(document); + + assert.deepStrictEqual(result, [ + { + code: "unknownAtRules", + message: "Unknown at rule @tailwind", + range: { + start: { + line: 1, + character: 0, + }, + end: { + line: 1, + character: 9, + }, + }, + severity: DiagnosticSeverity.Warning, + source: "scss", + }, + ]); +}); + +test("does not lint if configured", async () => { + const document = fileSystemProvider.createDocument(` +@tailwind base; +`); + + ls.configure({ + ...defaultConfiguration, + scss: { + ...defaultConfiguration.scss, + diagnostics: { + ...defaultConfiguration.scss.diagnostics, + lint: { + ...defaultConfiguration.scss.diagnostics.lint, + enabled: false, + }, + }, + }, + }); + const result = await ls.doDiagnostics(document); + + assert.deepStrictEqual(result, []); +}); + +test("ignores unknown at-rules if configured", async () => { + const document = fileSystemProvider.createDocument(` +@tailwind base; +`); + + ls.configure({ + ...defaultConfiguration, + scss: { + ...defaultConfiguration.scss, + diagnostics: { + ...defaultConfiguration.scss.diagnostics, + lint: { + ...defaultConfiguration.scss.diagnostics.lint, + unknownAtRules: "ignore", + }, + }, + }, + }); + const result = await ls.doDiagnostics(document); + + assert.deepStrictEqual(result, []); +}); diff --git a/packages/language-services/src/language-feature.ts b/packages/language-services/src/language-feature.ts index e912b06b..124a985c 100644 --- a/packages/language-services/src/language-feature.ts +++ b/packages/language-services/src/language-feature.ts @@ -5,14 +5,11 @@ import { Scanner, SassScanner, } from "@somesass/vscode-css-languageservice"; -import merge from "lodash.merge"; -import { defaultConfiguration } from "./configuration"; import { LanguageModelCache } from "./language-model-cache"; import { LanguageServiceOptions, TextDocument, LanguageService, - LanguageServerConfiguration, NodeType, Range, SassDocumentSymbol, @@ -22,14 +19,13 @@ import { VariableDeclaration, URI, Utils, - ClientCapabilities, - RecursivePartial, LanguageConfiguration, + LanguageServerConfiguration, + ClientCapabilities, } from "./language-services-types"; import { asDollarlessVariable } from "./utils/sass"; export type LanguageFeatureInternal = { - cache: LanguageModelCache; cssLs: VSCodeLanguageService; sassLs: VSCodeLanguageService; scssLs: VSCodeLanguageService; @@ -52,13 +48,19 @@ type FindOptions = { export abstract class LanguageFeature { protected ls; protected options; - protected clientCapabilities: ClientCapabilities; - protected configuration: LanguageServerConfiguration = defaultConfiguration; private _internal: LanguageFeatureInternal; protected get cache(): LanguageModelCache { - return this._internal.cache; + return this.ls.cache; + } + + protected get configuration(): LanguageServerConfiguration { + return this.ls.configuration; + } + + protected get clientCapabilities(): ClientCapabilities { + return this.ls.clientCapabilities; } constructor( @@ -68,59 +70,25 @@ export abstract class LanguageFeature { ) { this.ls = ls; this.options = options; - this.clientCapabilities = options.clientCapabilities; this._internal = _internal; } languageConfiguration(document: TextDocument): LanguageConfiguration { switch (document.languageId) { case "css": { - return this.configuration.css; + return this.ls.configuration.css; } case "sass": { - return this.configuration.sass; + return this.ls.configuration.sass; } case "scss": { - return this.configuration.scss; + return this.ls.configuration.scss; } } throw new Error(`Unsupported language ${document.languageId}`); } - configure( - configuration: RecursivePartial, - ): void { - this.configuration = merge(defaultConfiguration, configuration); - - this._internal.sassLs.configure({ - validate: this.configuration.sass.diagnostics.enabled, - lint: this.configuration.sass.diagnostics.lint, - completion: this.configuration.sass.completion, - hover: this.configuration.sass.hover, - importAliases: this.configuration.workspace.importAliases, - loadPaths: this.configuration.workspace.loadPaths, - }); - - this._internal.scssLs.configure({ - validate: this.configuration.scss.diagnostics.enabled, - lint: this.configuration.scss.diagnostics.lint, - completion: this.configuration.scss.completion, - hover: this.configuration.scss.hover, - importAliases: this.configuration.workspace.importAliases, - loadPaths: this.configuration.workspace.loadPaths, - }); - - this._internal.cssLs.configure({ - validate: this.configuration.css.diagnostics.enabled, - lint: this.configuration.css.diagnostics.lint, - completion: this.configuration.css.completion, - hover: this.configuration.css.hover, - importAliases: this.configuration.workspace.importAliases, - loadPaths: this.configuration.workspace.loadPaths, - }); - } - protected getUpstreamLanguageServer( document: TextDocument, ): VSCodeLanguageService { @@ -140,9 +108,12 @@ export abstract class LanguageFeature { * @returns The resolved path */ resolveReference: (ref: string, base: string) => { - if (ref.startsWith("/") && this.configuration.workspace.workspaceRoot) { + if ( + ref.startsWith("/") && + this.ls.configuration.workspace.workspaceRoot + ) { return Utils.joinPath( - this.configuration.workspace.workspaceRoot, + this.ls.configuration.workspace.workspaceRoot, ref, ).toString(true); } diff --git a/packages/language-services/src/language-services-types.ts b/packages/language-services/src/language-services-types.ts index f451d2c9..5cb90363 100644 --- a/packages/language-services/src/language-services-types.ts +++ b/packages/language-services/src/language-services-types.ts @@ -25,6 +25,7 @@ import { FoldingRange, FoldingRangeKind, SelectionRange, + ICSSDataProvider, } from "@somesass/vscode-css-languageservice"; import type { ParseResult } from "sassdoc-parser"; import { TextDocument } from "vscode-languageserver-textdocument"; @@ -63,6 +64,7 @@ import { } from "vscode-languageserver-types"; import { URI, Utils } from "vscode-uri"; import { FoldingRangeContext } from "./features/folding-ranges"; +import { LanguageModelCache } from "./language-model-cache"; /** * The root of the abstract syntax tree. @@ -83,6 +85,11 @@ export type RecursivePartial = { }; export interface LanguageService { + readonly cache: LanguageModelCache; + readonly clientCapabilities: ClientCapabilities; + readonly configuration: LanguageServerConfiguration; + readonly fs: FileSystemProvider; + /** * Clears all cached documents, forcing everything to be reparsed the next time a feature is used. */ @@ -179,8 +186,21 @@ export interface LanguageService { ): Promise< null | { defaultBehavior: boolean } | { range: Range; placeholder: string } >; + /** + * Load custom data sets, for example custom at-rules, for use in completions and diagnostics. + * + * @see https://github.com/microsoft/vscode-css-languageservice/blob/main/docs/customData.md + */ + setDataProviders( + customDataProviders: ICSSDataProvider[], + options?: SetDataProvidersOptions, + ): void; } +export type SetDataProvidersOptions = { + useDefaultProviders: boolean; +}; + export type Rename = | { range: Range; placeholder: string } | { defaultBehavior: boolean }; diff --git a/packages/language-services/src/language-services.ts b/packages/language-services/src/language-services.ts index 6166d802..6a85d474 100644 --- a/packages/language-services/src/language-services.ts +++ b/packages/language-services/src/language-services.ts @@ -1,7 +1,10 @@ import { getCSSLanguageService, getSassLanguageService, + ICSSDataProvider, + LanguageService as UpstreamLanguageService, } from "@somesass/vscode-css-languageservice"; +import merge from "lodash.merge"; import { defaultConfiguration } from "./configuration"; import { CodeActions } from "./features/code-actions"; import { DoComplete } from "./features/do-complete"; @@ -17,7 +20,7 @@ import { FindReferences } from "./features/find-references"; import { FindSymbols } from "./features/find-symbols"; import { FoldingRangeContext, FoldingRanges } from "./features/folding-ranges"; import { SelectionRanges } from "./features/selection-ranges"; -import { LanguageModelCache as LanguageServerCache } from "./language-model-cache"; +import { LanguageModelCache } from "./language-model-cache"; import { CodeActionContext, LanguageService, @@ -34,6 +37,9 @@ import { Range, ReferenceContext, URI, + SetDataProvidersOptions, + RecursivePartial, + ClientCapabilities, } from "./language-services-types"; import { mapFsProviders } from "./utils/fs-provider"; @@ -55,7 +61,6 @@ export function getLanguageService( } class LanguageServiceImpl implements LanguageService { - #cache: LanguageServerCache; #codeActions: CodeActions; #doComplete: DoComplete; #doDiagnostics: DoDiagnostics; @@ -71,27 +76,55 @@ class LanguageServiceImpl implements LanguageService { #foldingRanges: FoldingRanges; #selectionRanges: SelectionRanges; + #configuration = defaultConfiguration; + get configuration(): LanguageServerConfiguration { + return this.#configuration; + } + + #cache: LanguageModelCache; + get cache(): LanguageModelCache { + return this.#cache; + } + + #clientCapabilities: ClientCapabilities; + get clientCapabilities(): ClientCapabilities { + return this.#clientCapabilities; + } + + #fs: FileSystemProvider; + get fs(): FileSystemProvider { + return this.#fs; + } + + #cssLs: UpstreamLanguageService; + #sassLs: UpstreamLanguageService; + #scssLs: UpstreamLanguageService; + constructor(options: LanguageServiceOptions) { + this.#clientCapabilities = options.clientCapabilities; + this.#fs = options.fileSystemProvider; + const vscodeLsOptions = { - clientCapabilities: options.clientCapabilities, + clientCapabilities: this.clientCapabilities, fileSystemProvider: mapFsProviders(options.fileSystemProvider), }; - const sassLs = getSassLanguageService(vscodeLsOptions); - const cache = new LanguageServerCache({ - sassLs, + this.#cssLs = getCSSLanguageService(vscodeLsOptions); + this.#sassLs = getSassLanguageService(vscodeLsOptions); + // The server code is the same as sassLs, but separate on syntax in case the user has different settings + this.#scssLs = getSassLanguageService(vscodeLsOptions); + + this.#cache = new LanguageModelCache({ + sassLs: this.#sassLs, ...options.languageModelCache, }); const internal = { - cache, - sassLs, - cssLs: getCSSLanguageService(vscodeLsOptions), - // The server code is the same as sassLs, but separate on syntax in case the user has different settings - scssLs: getSassLanguageService(vscodeLsOptions), + cssLs: this.#cssLs, + sassLs: this.#sassLs, + scssLs: this.#scssLs, }; - this.#cache = cache; this.#codeActions = new CodeActions(this, options, internal); this.#doComplete = new DoComplete(this, options, internal); this.#doDiagnostics = new DoDiagnostics(this, options, internal); @@ -112,21 +145,46 @@ class LanguageServiceImpl implements LanguageService { this.#selectionRanges = new SelectionRanges(this, options, internal); } - configure(configuration: LanguageServerConfiguration): void { - this.#codeActions.configure(configuration); - this.#doComplete.configure(configuration); - this.#doDiagnostics.configure(configuration); - this.#doHover.configure(configuration); - this.#doRename.configure(configuration); - this.#doSignatureHelp.configure(configuration); - this.#findColors.configure(configuration); - this.#findDefinition.configure(configuration); - this.#findDocumentHighlights.configure(configuration); - this.#findDocumentLinks.configure(configuration); - this.#findReferences.configure(configuration); - this.#findSymbols.configure(configuration); - this.#foldingRanges.configure(configuration); - this.#selectionRanges.configure(configuration); + configure( + configuration: RecursivePartial, + ): void { + this.#configuration = merge(defaultConfiguration, configuration); + + this.#sassLs.configure({ + validate: this.configuration.sass.diagnostics.enabled, + lint: this.configuration.sass.diagnostics.lint, + completion: this.configuration.sass.completion, + hover: this.configuration.sass.hover, + importAliases: this.configuration.workspace.importAliases, + loadPaths: this.configuration.workspace.loadPaths, + }); + + this.#scssLs.configure({ + validate: this.configuration.scss.diagnostics.enabled, + lint: this.configuration.scss.diagnostics.lint, + completion: this.configuration.scss.completion, + hover: this.configuration.scss.hover, + importAliases: this.configuration.workspace.importAliases, + loadPaths: this.configuration.workspace.loadPaths, + }); + + this.#cssLs.configure({ + validate: this.configuration.css.diagnostics.enabled, + lint: this.configuration.css.diagnostics.lint, + completion: this.configuration.css.completion, + hover: this.configuration.css.hover, + importAliases: this.configuration.workspace.importAliases, + loadPaths: this.configuration.workspace.loadPaths, + }); + } + + setDataProviders( + providers: ICSSDataProvider[], + options: SetDataProvidersOptions = { useDefaultProviders: true }, + ): void { + this.#cssLs.setDataProviders(options.useDefaultProviders, providers); + this.#sassLs.setDataProviders(options.useDefaultProviders, providers); + this.#scssLs.setDataProviders(options.useDefaultProviders, providers); } parseStylesheet(document: TextDocument) { diff --git a/packages/vscode-css-languageservice/src/services/cssValidation.ts b/packages/vscode-css-languageservice/src/services/cssValidation.ts index afe13ab7..dfcccd9d 100644 --- a/packages/vscode-css-languageservice/src/services/cssValidation.ts +++ b/packages/vscode-css-languageservice/src/services/cssValidation.ts @@ -30,15 +30,12 @@ export class CSSValidation { const entries: nodes.IMarker[] = []; entries.push.apply(entries, nodes.ParseErrorCollector.entries(stylesheet)); - entries.push.apply( - entries, - LintVisitor.entries( - stylesheet, - document, - new LintConfigurationSettings(settings && settings.lint !== false ? settings.lint : undefined), - this.cssDataManager, - ), - ); + if (settings && settings.lint !== false) { + entries.push.apply( + entries, + LintVisitor.entries(stylesheet, document, new LintConfigurationSettings(settings.lint), this.cssDataManager), + ); + } const ruleIds: string[] = []; for (const r in Rules) {