From 94fb796fa9794dc2a10e30482d0d0409efdd82e3 Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 29 Apr 2022 10:46:18 -0400 Subject: [PATCH 1/3] Overhaul config to new pull model --- package.json | 2 +- .../src/core/config/ConfigManager.ts | 142 +++++++++++++----- .../src/core/config/interfaces.ts | 53 ------- .../src/plugins/css/CSSPlugin.ts | 94 ++++++------ .../src/plugins/html/HTMLPlugin.ts | 42 +++--- .../plugins/html/features/astro-attributes.ts | 1 + .../plugins/typescript/TypeScriptPlugin.ts | 4 +- .../features/CodeActionsProvider.ts | 21 ++- .../features/CompletionsProvider.ts | 45 +++--- packages/language-server/src/server.ts | 34 +++-- packages/vscode/package.json | 108 ++++++++++--- packages/vscode/src/index.ts | 20 +-- yarn.lock | 7 +- 13 files changed, 347 insertions(+), 226 deletions(-) diff --git a/package.json b/package.json index 4bd1bc7b..91338078 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "eslint-plugin-prettier": "^3.4.0", "turbo": "^1.0.0", "prettier": "^2.2.1", - "typescript": "^4.5.4" + "typescript": "^4.6.0" }, "engines": { "node": "^14.16.0 || >=16.0.0" diff --git a/packages/language-server/src/core/config/ConfigManager.ts b/packages/language-server/src/core/config/ConfigManager.ts index d6e67fde..8f1a124e 100644 --- a/packages/language-server/src/core/config/ConfigManager.ts +++ b/packages/language-server/src/core/config/ConfigManager.ts @@ -1,43 +1,29 @@ import { get, merge } from 'lodash'; import { VSCodeEmmetConfig } from '@vscode/emmet-helper'; -import { LSConfig } from './interfaces'; +import { LSConfig, LSCSSConfig, LSHTMLConfig, LSTypescriptConfig } from './interfaces'; +import { Connection, DidChangeConfigurationParams } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { FormatCodeSettings, TsConfigSourceFile, UserPreferences } from 'typescript'; const defaultLSConfig: LSConfig = { - astro: { - enabled: true, - diagnostics: { enabled: true }, - rename: { enabled: true }, - format: { enabled: true }, - completions: { enabled: true }, - hover: { enabled: true }, - codeActions: { enabled: true }, - selectionRange: { enabled: true }, - }, typescript: { enabled: true, diagnostics: { enabled: true }, hover: { enabled: true }, completions: { enabled: true }, definitions: { enabled: true }, - findReferences: { enabled: true }, documentSymbols: { enabled: true }, codeActions: { enabled: true }, rename: { enabled: true }, - selectionRange: { enabled: true }, signatureHelp: { enabled: true }, semanticTokens: { enabled: true }, - implementation: { enabled: true }, - typeDefinition: { enabled: true }, }, css: { enabled: true, - diagnostics: { enabled: true }, hover: { enabled: true }, completions: { enabled: true, emmet: true }, documentColors: { enabled: true }, - colorPresentations: { enabled: true }, documentSymbols: { enabled: true }, - selectionRange: { enabled: true }, }, html: { enabled: true, @@ -45,8 +31,6 @@ const defaultLSConfig: LSConfig = { completions: { enabled: true, emmet: true }, tagComplete: { enabled: true }, documentSymbols: { enabled: true }, - renameTags: { enabled: true }, - linkedEditing: { enabled: true }, }, }; @@ -62,24 +46,70 @@ type DeepPartial = T extends Record * For more info on this, see the [internal docs](../../../../../docs/internal/language-server/config.md) */ export class ConfigManager { - private config: LSConfig = defaultLSConfig; - private emmetConfig: VSCodeEmmetConfig = {}; + private config = defaultLSConfig; + private documentSettings: Record>> = {}; private isTrusted = true; - updateConfig(config: DeepPartial): void { - // Ideally we shouldn't need the merge here because all updates should be valid and complete configs. - // But since those configs come from the client they might be out of synch with the valid config: - // We might at some point in the future forget to synch config settings in all packages after updating the config. - this.config = merge({}, defaultLSConfig, this.config, config); + private connection: Connection; + + constructor(connection: Connection) { + this.connection = connection; + } + + updateConfig() { + // Reset all cached document settings + this.documentSettings = {}; + } + + removeDocument(scopeUri: string) { + delete this.documentSettings[scopeUri]; + } + + async getConfig(section: string, scopeUri: string): Promise> { + if (!this.documentSettings[scopeUri]) { + this.documentSettings[scopeUri] = {}; + } + + if (!this.documentSettings[scopeUri][section]) { + this.documentSettings[scopeUri][section] = await this.connection.workspace.getConfiguration({ + scopeUri, + section, + }); + } + + return this.documentSettings[scopeUri][section]; } - updateEmmetConfig(config: VSCodeEmmetConfig) { - this.emmetConfig = config || {}; + async getEmmetConfig(document: TextDocument): Promise { + const emmetConfig = (await this.getConfig('emmet', document.uri)) ?? {}; + + return emmetConfig; } - getEmmetConfig(): VSCodeEmmetConfig { - return this.emmetConfig; + async getTSFormatConfig(document: TextDocument): Promise { + const formatConfig = (await this.getConfig('typescript.format', document.uri)) ?? {}; + + return formatConfig; + } + + async getTSPreferences(document: TextDocument): Promise { + const config = (await this.getConfig('typescript', document.uri)) ?? {}; + const preferences = config['preferences']; + + return { + quotePreference: getQuoteStylePreference(preferences), + importModuleSpecifierPreference: getImportModuleSpecifierPreference(preferences), + importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(preferences), + allowTextChangesInNewFiles: document.uri.startsWith('file://'), + providePrefixAndSuffixTextForRename: + (preferences.renameShorthandProperties ?? true) === false ? false : preferences.useAliasesForRenames ?? true, + includeAutomaticOptionalChainCompletions: config.suggest?.includeAutomaticOptionalChainCompletions ?? true, + includeCompletionsForImportStatements: config.suggest?.includeCompletionsForImportStatements ?? true, + includeCompletionsWithSnippetText: config.suggest?.includeCompletionsWithSnippetText ?? true, + includeCompletionsForModuleExports: config.suggest?.autoImports ?? true, + allowIncompleteCompletions: true, + }; } /** @@ -90,6 +120,16 @@ export class ConfigManager { return !!this.get(key); } + async isEnabled( + document: TextDocument, + plugin: keyof LSConfig, + feature?: keyof LSTypescriptConfig | keyof LSCSSConfig | keyof LSHTMLConfig + ) { + const config = await this.getConfig('astro', document.uri); + + return feature ? config[plugin][feature]['enabled'] : config[plugin]['enabled']; + } + /** * Get a specific setting value * @param key a string which is a path. Example: 'astro.diagnostics.enable'. @@ -97,11 +137,41 @@ export class ConfigManager { get(key: string): T { return get(this.config, key); } +} - /** - * Get the entire user configuration - */ - getFullConfig(): Readonly { - return this.config; +function getQuoteStylePreference(config: any) { + switch (config.quoteStyle as string) { + case 'single': + return 'single'; + case 'double': + return 'double'; + default: + return 'auto'; + } +} + +function getImportModuleSpecifierPreference(config: any) { + switch (config.importModuleSpecifier as string) { + case 'project-relative': + return 'project-relative'; + case 'relative': + return 'relative'; + case 'non-relative': + return 'non-relative'; + default: + return undefined; + } +} + +function getImportModuleSpecifierEndingPreference(config: any) { + switch (config.importModuleSpecifierEnding as string) { + case 'minimal': + return 'minimal'; + case 'index': + return 'index'; + case 'js': + return 'js'; + default: + return 'auto'; } } diff --git a/packages/language-server/src/core/config/interfaces.ts b/packages/language-server/src/core/config/interfaces.ts index fed42d41..f824e4cf 100644 --- a/packages/language-server/src/core/config/interfaces.ts +++ b/packages/language-server/src/core/config/interfaces.ts @@ -3,37 +3,11 @@ * Make sure that this is kept in sync with the `package.json` of the VS Code extension */ export interface LSConfig { - astro: LSAstroConfig; typescript: LSTypescriptConfig; html: LSHTMLConfig; css: LSCSSConfig; } -export interface LSAstroConfig { - enabled: boolean; - diagnostics: { - enabled: boolean; - }; - format: { - enabled: boolean; - }; - rename: { - enabled: boolean; - }; - completions: { - enabled: boolean; - }; - hover: { - enabled: boolean; - }; - codeActions: { - enabled: boolean; - }; - selectionRange: { - enabled: boolean; - }; -} - export interface LSTypescriptConfig { enabled: boolean; diagnostics: { @@ -48,9 +22,6 @@ export interface LSTypescriptConfig { completions: { enabled: boolean; }; - findReferences: { - enabled: boolean; - }; definitions: { enabled: boolean; }; @@ -60,21 +31,12 @@ export interface LSTypescriptConfig { rename: { enabled: boolean; }; - selectionRange: { - enabled: boolean; - }; signatureHelp: { enabled: boolean; }; semanticTokens: { enabled: boolean; }; - implementation: { - enabled: boolean; - }; - typeDefinition: { - enabled: boolean; - }; } export interface LSHTMLConfig { @@ -92,19 +54,10 @@ export interface LSHTMLConfig { documentSymbols: { enabled: boolean; }; - renameTags: { - enabled: boolean; - }; - linkedEditing: { - enabled: boolean; - }; } export interface LSCSSConfig { enabled: boolean; - diagnostics: { - enabled: boolean; - }; hover: { enabled: boolean; }; @@ -115,13 +68,7 @@ export interface LSCSSConfig { documentColors: { enabled: boolean; }; - colorPresentations: { - enabled: boolean; - }; documentSymbols: { enabled: boolean; }; - selectionRange: { - enabled: boolean; - }; } diff --git a/packages/language-server/src/plugins/css/CSSPlugin.ts b/packages/language-server/src/plugins/css/CSSPlugin.ts index 11471c2f..c1a9a192 100644 --- a/packages/language-server/src/plugins/css/CSSPlugin.ts +++ b/packages/language-server/src/plugins/css/CSSPlugin.ts @@ -12,7 +12,7 @@ import { SymbolInformation, } from 'vscode-languageserver'; import { ConfigManager } from '../../core/config/ConfigManager'; -import { LSCSSConfig } from '../../core/config/interfaces'; +import { LSConfig, LSCSSConfig } from '../../core/config/interfaces'; import { AstroDocument, isInsideFrontmatter, @@ -46,8 +46,8 @@ export class CSSPlugin implements Plugin { this.configManager = configManager; } - doHover(document: AstroDocument, position: Position): Hover | null { - if (!this.featureEnabled('hover')) { + async doHover(document: AstroDocument, position: Position): Promise { + if (!(await this.featureEnabled(document, 'hover'))) { return null; } @@ -98,12 +98,12 @@ export class CSSPlugin implements Plugin { return hoverInfo ? mapHoverToParent(cssDocument, hoverInfo) : hoverInfo; } - getCompletions( + async getCompletions( document: AstroDocument, position: Position, completionContext?: CompletionContext - ): CompletionList | null { - if (!this.featureEnabled('completions')) { + ): Promise { + if (!(await this.featureEnabled(document, 'completions'))) { return null; } @@ -133,7 +133,7 @@ export class CSSPlugin implements Plugin { if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) { const [start, end] = attributeContext.valueRange; - return this.getCompletionsInternal(document, position, new StyleAttributeDocument(document, start, end)); + return await this.getCompletionsInternal(document, position, new StyleAttributeDocument(document, start, end)); } // If we're not in a style attribute, instead give completions for ids and classes used in the current document else if ((attributeContext.name == 'id' || attributeContext.name == 'class') && attributeContext.inValue) { @@ -145,14 +145,16 @@ export class CSSPlugin implements Plugin { } const cssDocument = this.getCSSDocumentForStyleTag(styleTag, document); - return this.getCompletionsInternal(document, position, cssDocument); + return await this.getCompletionsInternal(document, position, cssDocument); } - private getCompletionsInternal(document: AstroDocument, position: Position, cssDocument: CSSDocumentBase) { + private async getCompletionsInternal(document: AstroDocument, position: Position, cssDocument: CSSDocumentBase) { + const emmetConfig = await this.configManager.getEmmetConfig(document); + if (isSASS(cssDocument)) { // The CSS language service does not support SASS (not to be confused with SCSS) // however we can at least still at least provide Emmet completions in SASS blocks - return getEmmetCompletions(document, position, 'sass', this.configManager.getEmmetConfig()) || null; + return getEmmetCompletions(document, position, 'sass', emmetConfig) || null; } const cssLang = extractLanguage(cssDocument); @@ -163,32 +165,35 @@ export class CSSPlugin implements Plugin { items: [], }; - langService.setCompletionParticipants([ - { - onCssProperty: (context) => { - if (context?.propertyName) { - emmetResults = - getEmmetCompletions( - cssDocument, - cssDocument.getGeneratedPosition(position), - getLanguage(cssLang), - this.configManager.getEmmetConfig() - ) || emmetResults; - } - }, - onCssPropertyValue: (context) => { - if (context?.propertyValue) { - emmetResults = - getEmmetCompletions( - cssDocument, - cssDocument.getGeneratedPosition(position), - getLanguage(cssLang), - this.configManager.getEmmetConfig() - ) || emmetResults; - } + const extensionConfig = await this.configManager.getConfig('astro', document.uri); + if (extensionConfig.css.completions.emmet) { + langService.setCompletionParticipants([ + { + onCssProperty: (context) => { + if (context?.propertyName) { + emmetResults = + getEmmetCompletions( + cssDocument, + cssDocument.getGeneratedPosition(position), + getLanguage(cssLang), + emmetConfig + ) || emmetResults; + } + }, + onCssPropertyValue: (context) => { + if (context?.propertyValue) { + emmetResults = + getEmmetCompletions( + cssDocument, + cssDocument.getGeneratedPosition(position), + getLanguage(cssLang), + emmetConfig + ) || emmetResults; + } + }, }, - }, - ]); + ]); + } const results = langService.doComplete( cssDocument, @@ -205,8 +210,8 @@ export class CSSPlugin implements Plugin { ); } - getDocumentColors(document: AstroDocument): ColorInformation[] { - if (!this.featureEnabled('documentColors')) { + async getDocumentColors(document: AstroDocument): Promise { + if (!(await this.featureEnabled(document, 'documentColors'))) { return []; } @@ -226,8 +231,8 @@ export class CSSPlugin implements Plugin { return flatten(allColorInfo); } - getColorPresentations(document: AstroDocument, range: Range, color: Color): ColorPresentation[] { - if (!this.featureEnabled('colorPresentations')) { + async getColorPresentations(document: AstroDocument, range: Range, color: Color): Promise { + if (!(await this.featureEnabled(document, 'documentColors'))) { return []; } @@ -261,8 +266,8 @@ export class CSSPlugin implements Plugin { return flatten(allFoldingRanges); } - getDocumentSymbols(document: AstroDocument): SymbolInformation[] { - if (!this.featureEnabled('documentSymbols')) { + async getDocumentSymbols(document: AstroDocument): Promise { + if (!(await this.featureEnabled(document, 'documentSymbols'))) { return []; } @@ -322,8 +327,11 @@ export class CSSPlugin implements Plugin { }); } - private featureEnabled(feature: keyof LSCSSConfig) { - return this.configManager.enabled('css.enabled') && this.configManager.enabled(`css.${feature}.enabled`); + private async featureEnabled(document: AstroDocument, feature: keyof LSCSSConfig) { + return ( + (await this.configManager.isEnabled(document, 'css')) && + (await this.configManager.isEnabled(document, 'css', feature)) + ); } } diff --git a/packages/language-server/src/plugins/html/HTMLPlugin.ts b/packages/language-server/src/plugins/html/HTMLPlugin.ts index f1a492bb..78ce6b9a 100644 --- a/packages/language-server/src/plugins/html/HTMLPlugin.ts +++ b/packages/language-server/src/plugins/html/HTMLPlugin.ts @@ -14,7 +14,7 @@ import type { Plugin } from '../interfaces'; import { ConfigManager } from '../../core/config/ConfigManager'; import { AstroDocument } from '../../core/documents/AstroDocument'; import { isInComponentStartTag, isInsideExpression, isInsideFrontmatter } from '../../core/documents/utils'; -import { LSHTMLConfig } from '../../core/config/interfaces'; +import { LSConfig, LSHTMLConfig } from '../../core/config/interfaces'; import { isPossibleComponent } from '../../utils'; import { astroAttributes, astroDirectives, classListAttribute } from './features/astro-attributes'; import { removeDataAttrCompletion } from './utils'; @@ -40,8 +40,8 @@ export class HTMLPlugin implements Plugin { this.configManager = configManager; } - doHover(document: AstroDocument, position: Position): Hover | null { - if (!this.featureEnabled('hover')) { + async doHover(document: AstroDocument, position: Position): Promise { + if (!(await this.featureEnabled(document, 'hover'))) { return null; } @@ -66,8 +66,8 @@ export class HTMLPlugin implements Plugin { /** * Get HTML completions */ - getCompletions(document: AstroDocument, position: Position): CompletionList | null { - if (!this.featureEnabled('completions')) { + async getCompletions(document: AstroDocument, position: Position): Promise { + if (!(await this.featureEnabled(document, 'completions'))) { return null; } @@ -88,13 +88,16 @@ export class HTMLPlugin implements Plugin { items: [], }; - this.lang.setCompletionParticipants([ - { - onHtmlContent: () => - (emmetResults = - getEmmetCompletions(document, position, 'html', this.configManager.getEmmetConfig()) || emmetResults), - }, - ]); + const emmetConfig = await this.configManager.getEmmetConfig(document); + const extensionConfig = await this.configManager.getConfig('astro', document.uri); + if (extensionConfig.html.completions.emmet) { + this.lang.setCompletionParticipants([ + { + onHtmlContent: () => + (emmetResults = getEmmetCompletions(document, position, 'html', emmetConfig) || emmetResults), + }, + ]); + } // If we're in a component starting tag, we do not want HTML language completions // as HTML attributes are not valid for components @@ -119,8 +122,8 @@ export class HTMLPlugin implements Plugin { return this.lang.getFoldingRanges(document); } - doTagComplete(document: AstroDocument, position: Position): string | null { - if (!this.featureEnabled('tagComplete')) { + async doTagComplete(document: AstroDocument, position: Position): Promise { + if (!(await this.featureEnabled(document, 'tagComplete'))) { return null; } @@ -138,8 +141,8 @@ export class HTMLPlugin implements Plugin { return this.lang.doTagComplete(document, position, html); } - getDocumentSymbols(document: AstroDocument): SymbolInformation[] { - if (!this.featureEnabled('documentSymbols')) { + async getDocumentSymbols(document: AstroDocument): Promise { + if (!(await this.featureEnabled(document, 'documentSymbols'))) { return []; } @@ -186,7 +189,10 @@ export class HTMLPlugin implements Plugin { } } - private featureEnabled(feature: keyof LSHTMLConfig) { - return this.configManager.enabled('html.enabled') && this.configManager.enabled(`html.${feature}.enabled`); + private async featureEnabled(document: AstroDocument, feature: keyof LSHTMLConfig) { + return ( + (await this.configManager.isEnabled(document, 'html')) && + (await this.configManager.isEnabled(document, 'html', feature)) + ); } } diff --git a/packages/language-server/src/plugins/html/features/astro-attributes.ts b/packages/language-server/src/plugins/html/features/astro-attributes.ts index e2c4e163..08a740b1 100644 --- a/packages/language-server/src/plugins/html/features/astro-attributes.ts +++ b/packages/language-server/src/plugins/html/features/astro-attributes.ts @@ -42,6 +42,7 @@ export const astroAttributes = newHTMLDataProvider('astro-attributes', { { name: 'is:raw', description: 'Instructs the Astro compiler to treat any children of this element as text', + valueSet: 'v', references: [ { name: 'Astro reference', diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index 1f9dd68e..c09d2aa5 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -65,8 +65,8 @@ export class TypeScriptPlugin implements Plugin { this.configManager = configManager; this.languageServiceManager = new LanguageServiceManager(docManager, workspaceUris, configManager); - this.codeActionsProvider = new CodeActionsProviderImpl(this.languageServiceManager); - this.completionProvider = new CompletionsProviderImpl(this.languageServiceManager); + this.codeActionsProvider = new CodeActionsProviderImpl(this.languageServiceManager, this.configManager); + this.completionProvider = new CompletionsProviderImpl(this.languageServiceManager, this.configManager); this.hoverProvider = new HoverProviderImpl(this.languageServiceManager); this.signatureHelpProvider = new SignatureHelpProviderImpl(this.languageServiceManager); this.diagnosticsProvider = new DiagnosticsProviderImpl(this.languageServiceManager); diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index e6f82d6b..eb24e040 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -11,20 +11,21 @@ import { TextDocumentEdit, TextEdit, } from 'vscode-languageserver-types'; +import { ConfigManager } from '../../../core/config'; import { AstroDocument, getLineAtPosition, mapRangeToOriginal } from '../../../core/documents'; import { modifyLines } from '../../../utils'; import { CodeActionsProvider } from '../../interfaces'; import { LanguageServiceManager } from '../LanguageServiceManager'; import { AstroSnapshotFragment } from '../snapshots/DocumentSnapshot'; import { checkEndOfFileCodeInsert, convertRange, removeAstroComponentSuffix, toVirtualAstroFilePath } from '../utils'; -import { codeActionChangeToTextEdit, completionOptions } from './CompletionsProvider'; +import { codeActionChangeToTextEdit } from './CompletionsProvider'; import { findContainingNode } from './utils'; // These are VS Code specific CodeActionKind so they're not in the language server protocol export const sortImportKind = `${CodeActionKind.Source}.sortImports`; export class CodeActionsProviderImpl implements CodeActionsProvider { - constructor(private languageServiceManager: LanguageServiceManager) {} + constructor(private languageServiceManager: LanguageServiceManager, private configManager: ConfigManager) {} async getCodeActions( document: AstroDocument, @@ -40,6 +41,9 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start)); const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end)); + const tsPreferences = await this.configManager.getTSPreferences(document); + const formatOptions = await this.configManager.getTSFormatConfig(document); + let result: CodeAction[] = []; if (cancellationToken?.isCancellationRequested) { @@ -69,8 +73,11 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { // We currently cannot support quick fix for unreachable code properly due to the way our TSX output is structured .filter((code) => code !== 7027); - let codeFixes = errorCodes.includes(2304) ? this.getComponentQuickFix(start, end, lang, filePath) : undefined; - codeFixes = codeFixes ?? lang.getCodeFixesAtPosition(filePath, start, end, errorCodes, {}, {}); + let codeFixes = errorCodes.includes(2304) + ? this.getComponentQuickFix(start, end, lang, filePath, formatOptions, tsPreferences) + : undefined; + codeFixes = + codeFixes ?? lang.getCodeFixesAtPosition(filePath, start, end, errorCodes, formatOptions, tsPreferences); const codeActions = codeFixes.map((fix) => codeFixToCodeAction(fix, context.diagnostics, context.only ? CodeActionKind.QuickFix : CodeActionKind.Empty) @@ -119,7 +126,9 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { start: number, end: number, lang: ts.LanguageService, - filePath: string + filePath: string, + formatOptions: ts.FormatCodeSettings, + tsPreferences: ts.UserPreferences ): readonly ts.CodeFixAction[] | undefined { const sourceFile = lang.getProgram()?.getSourceFile(filePath); @@ -144,7 +153,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { const tagName = node.tagName; // Unlike quick fixes, completions will be able to find the component, so let's use those to get it - const completion = lang.getCompletionsAtPosition(filePath, tagName.getEnd(), completionOptions); + const completion = lang.getCompletionsAtPosition(filePath, tagName.getEnd(), tsPreferences, formatOptions); if (!completion) { return; diff --git a/packages/language-server/src/plugins/typescript/features/CompletionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionsProvider.ts index 5868dd83..fb98138c 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionsProvider.ts @@ -30,17 +30,18 @@ import { getRegExpMatches, isNotNullOrUndefined } from '../../../utils'; import { flatten } from 'lodash'; import { getMarkdownDocumentation } from '../previewer'; import { isPartOfImportStatement } from './utils'; - -export const completionOptions: ts.GetCompletionsAtPositionOptions = { - importModuleSpecifierPreference: 'relative', - importModuleSpecifierEnding: 'auto', - quotePreference: 'single', - includeCompletionsForModuleExports: true, - includeCompletionsForImportStatements: true, - includeCompletionsWithInsertText: true, - allowIncompleteCompletions: true, - includeCompletionsWithSnippetText: true, -}; +import { ConfigManager } from '../../../core/config'; + +// export const completionOptions: ts.GetCompletionsAtPositionOptions = { +// importModuleSpecifierPreference: 'relative', +// importModuleSpecifierEnding: 'auto', +// quotePreference: 'single', +// includeCompletionsForModuleExports: true, +// includeCompletionsForImportStatements: true, +// includeCompletionsWithInsertText: true, +// allowIncompleteCompletions: true, +// includeCompletionsWithSnippetText: true, +// }; /** * The language service throws an error if the character is not a valid trigger character. @@ -66,7 +67,7 @@ export interface CompletionItemData extends TextDocumentIdentifier { } export class CompletionsProviderImpl implements CompletionsProvider { - constructor(private languageServiceManager: LanguageServiceManager) {} + constructor(private languageServiceManager: LanguageServiceManager, private configManager: ConfigManager) {} private readonly validTriggerCharacters = ['.', '"', "'", '`', '/', '@', '<', '#'] as const; @@ -132,13 +133,21 @@ export class CompletionsProviderImpl implements CompletionsProvider> { const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document); + const tsPreferences = await this.configManager.getTSPreferences(document); + const data: CompletionItemData | undefined = item.data as any; if (!data || !data.filePath || cancellationToken?.isCancellationRequested) { @@ -188,7 +199,7 @@ export class CompletionsProviderImpl implements CompletionsProvider { const workspaceUris = params.workspaceFolders?.map((folder) => folder.uri.toString()) ?? [params.rootUri ?? '']; @@ -69,16 +70,6 @@ export function startLanguageServer(connection: vscode.Connection) { pluginHost.registerPlugin(new TypeScriptPlugin(documentManager, configManager, workspaceUris)); } - // Update language-server config with what the user supplied to us at launch - let astroConfiguration = params.initializationOptions?.configuration?.astro; - if (astroConfiguration) { - configManager.updateConfig(astroConfiguration); - } - let emmetConfiguration = params.initializationOptions?.configuration?.emmet; - if (emmetConfiguration) { - configManager.updateEmmetConfig(emmetConfiguration); - } - return { capabilities: { textDocumentSync: { @@ -146,10 +137,14 @@ export function startLanguageServer(connection: vscode.Connection) { }; }); - // On update of the user configuration of the language-server - connection.onDidChangeConfiguration(({ settings }: vscode.DidChangeConfigurationParams) => { - configManager.updateConfig(settings.astro); - configManager.updateEmmetConfig(settings.emmet); + // The params don't matter here because in "pull mode" it's always null, it's intended that when the config is updated + // you should just reset "your internal cache" and get the config again for relevant documents, weird API design + connection.onDidChangeConfiguration(async () => { + configManager.updateConfig(); + + documentManager.getAllOpenedByClient().forEach(async (document) => { + await configManager.getConfig('astro', document[1].uri); + }); }); // Documents @@ -244,11 +239,18 @@ export function startLanguageServer(connection: vscode.Connection) { 'documentChange', debounceThrottle(async (document: AstroDocument) => diagnosticsManager.update(document), 1000) ); - documentManager.on('documentClose', (document: AstroDocument) => diagnosticsManager.removeDiagnostics(document)); + + documentManager.on('documentClose', (document: AstroDocument) => { + diagnosticsManager.removeDiagnostics(document); + configManager.removeDocument(document.uri); + }); // Taking off 🚀 connection.onInitialized(() => { connection.console.log('Successfully initialized! 🚀'); + + // Register for all configuration changes. + connection.client.register(DidChangeConfigurationNotification.type); }); connection.listen(); diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 827ba375..2c71ce1c 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -77,77 +77,137 @@ "default": "off", "description": "Traces the communication between VS Code and the language server." }, - "astro.plugin.typescript.enable": { + "astro.typescript.enabled": { "type": "boolean", "default": true, "title": "TypeScript", - "description": "Enable the TypeScript plugin" + "description": "Enable TypeScript features" }, - "astro.plugin.typescript.diagnostics.enable": { + "astro.typescript.diagnostics.enabled": { "type": "boolean", "default": true, "title": "TypeScript: Diagnostics", "description": "Enable diagnostic messages for TypeScript" }, - "astro.plugin.typescript.hover.enable": { + "astro.typescript.hover.enabled": { "type": "boolean", "default": true, "title": "TypeScript: Hover Info", "description": "Enable hover info for TypeScript" }, - "astro.plugin.typescript.documentSymbols.enable": { + "astro.typescript.documentSymbols.enabled": { "type": "boolean", "default": true, "title": "TypeScript: Symbols in Outline", "description": "Enable document symbols for TypeScript" }, - "astro.plugin.typescript.completions.enable": { + "astro.typescript.completions.enabled": { "type": "boolean", "default": true, "title": "TypeScript: Completions", "description": "Enable completions for TypeScript" }, - "astro.plugin.typescript.findReferences.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Find References", - "description": "Enable find-references for TypeScript" - }, - "astro.plugin.typescript.definitions.enable": { + "astro.typescript.definitions.enabled": { "type": "boolean", "default": true, "title": "TypeScript: Go to Definition", "description": "Enable go to definition for TypeScript" }, - "astro.plugin.typescript.codeActions.enable": { + "astro.typescript.codeActions.enabled": { "type": "boolean", "default": true, "title": "TypeScript: Code Actions", "description": "Enable code actions for TypeScript" }, - "astro.plugin.typescript.selectionRange.enable": { - "type": "boolean", - "default": true, - "title": "TypeScript: Selection Range", - "description": "Enable selection range for TypeScript" - }, - "astro.plugin.typescript.signatureHelp.enable": { + "astro.typescript.signatureHelp.enabled": { "type": "boolean", "default": true, "title": "TypeScript: Signature Help", "description": "Enable signature help (parameter hints) for TypeScript" }, - "astro.plugin.typescript.rename.enable": { + "astro.typescript.rename.enabled": { "type": "boolean", "default": true, "title": "TypeScript: Rename", "description": "Enable rename functionality for JS/TS variables inside Astro files" }, - "astro.plugin.typescript.semanticTokens.enable": { + "astro.typescript.semanticTokens.enabled": { "type": "boolean", "default": true, "title": "TypeScript: Semantic Tokens", - "description": "Enable semantic tokens (semantic highlight) for TypeScript." + "description": "Enable semantic tokens (used for semantic highlighting) for TypeScript." + }, + "astro.html.enabled": { + "type": "boolean", + "default": true, + "title": "HTML", + "description": "Enable HTML features" + }, + "astro.html.hover.enabled": { + "type": "boolean", + "default": true, + "title": "HTML: Hover Info", + "description": "Enable hover info for HTML" + }, + "astro.html.completions.enabled": { + "type": "boolean", + "default": true, + "title": "HTML: Completions", + "description": "Enable completions for HTML" + }, + "astro.html.completions.emmet": { + "type": "boolean", + "default": true, + "title": "HTML: Emmet Completions", + "description": "Enable Emmet completions for HTML" + }, + "astro.html.tagComplete.enabled": { + "type": "boolean", + "default": true, + "title": "HTML: Tag Completion", + "description": "Enable tag completion for HTML" + }, + "astro.html.documentSymbols.enabled": { + "type": "boolean", + "default": true, + "title": "HTML: Symbols in Outline", + "description": "Enable document symbols for CSS" + }, + "astro.css.enabled": { + "type": "boolean", + "default": true, + "title": "CSS", + "description": "Enable CSS features" + }, + "astro.css.hover.enabled": { + "type": "boolean", + "default": true, + "title": "CSS: Hover Info", + "description": "Enable hover info for CSS" + }, + "astro.css.completions.enabled": { + "type": "boolean", + "default": true, + "title": "CSS: Completions", + "description": "Enable completions for CSS" + }, + "astro.css.completions.emmet": { + "type": "boolean", + "default": true, + "title": "CSS: Emmet Completions", + "description": "Enable Emmet completions for CSS" + }, + "astro.css.documentColors.enabled": { + "type": "boolean", + "default": true, + "title": "CSS: Document Colors", + "description": "Enable color picker for CSS" + }, + "astro.css.documentSymbols.enabled": { + "type": "boolean", + "default": true, + "title": "CSS: Symbols in Outline", + "description": "Enable document symbols for CSS" } } }, diff --git a/packages/vscode/src/index.ts b/packages/vscode/src/index.ts index d4d3a1b9..ab8bda04 100644 --- a/packages/vscode/src/index.ts +++ b/packages/vscode/src/index.ts @@ -11,6 +11,8 @@ import { activateTagClosing } from './html/autoClose.js'; const TagCloseRequest: RequestType = new RequestType('html/tag'); +let client: LanguageClient; + export async function activate(context: ExtensionContext) { const serverModule = require.resolve('@astrojs/language-server/bin/nodeServer.js'); @@ -29,24 +31,16 @@ export async function activate(context: ExtensionContext) { const clientOptions: LanguageClientOptions = { documentSelector: [{ scheme: 'file', language: 'astro' }], synchronize: { - configurationSection: ['astro', 'javascript', 'typescript', 'prettier', 'emmet'], fileEvents: workspace.createFileSystemWatcher('{**/*.js,**/*.ts}', false, false, false), }, initializationOptions: { - configuration: { - astro: workspace.getConfiguration('astro'), - prettier: workspace.getConfiguration('prettier'), - emmet: workspace.getConfiguration('emmet'), - typescript: workspace.getConfiguration('typescript'), - javascript: workspace.getConfiguration('javascript'), - }, environment: 'node', dontFilterIncompleteCompletions: true, // VSCode filters client side and is smarter at it than us isTrusted: workspace.isTrusted, }, }; - let client = createLanguageServer(serverOptions, clientOptions); + client = createLanguageServer(serverOptions, clientOptions); context.subscriptions.push(client.start()); client @@ -130,6 +124,14 @@ export async function activate(context: ExtensionContext) { }; } +export function deactivate(): Promise | undefined { + if (!client) { + return undefined; + } + + return client.stop(); +} + function createLanguageServer(serverOptions: ServerOptions, clientOptions: LanguageClientOptions) { return new LanguageClient('astro', 'Astro', serverOptions, clientOptions); } diff --git a/yarn.lock b/yarn.lock index e0dc1f51..b5904032 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5482,11 +5482,16 @@ type-fest@^2.5.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.12.2.tgz#80a53614e6b9b475eb9077472fb7498dc7aa51d0" integrity sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ== -typescript@*, typescript@^4.5.4, typescript@~4.6.2: +typescript@*, typescript@~4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz" integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== +typescript@^4.6.0: + version "4.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== + unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz" From b2c7134d6f9684be11a190fa40ec5f024e50062f Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 29 Apr 2022 13:21:01 -0400 Subject: [PATCH 2/3] Update tests --- .../src/core/config/ConfigManager.ts | 68 +++++++++---- .../plugins/typescript/TypeScriptPlugin.ts | 21 ++-- packages/language-server/src/server.ts | 27 +++-- .../test/plugins/css/CSSPlugin.test.ts | 99 ++++++++++--------- .../test/plugins/html/HTMLPlugin.test.ts | 57 ++++++----- .../typescript/TypeScriptPlugin.test.ts | 25 +++-- .../features/CodeActionsProvider.test.ts | 2 +- .../features/CompletionsProvider.test.ts | 10 +- 8 files changed, 178 insertions(+), 131 deletions(-) diff --git a/packages/language-server/src/core/config/ConfigManager.ts b/packages/language-server/src/core/config/ConfigManager.ts index 8f1a124e..96e6f443 100644 --- a/packages/language-server/src/core/config/ConfigManager.ts +++ b/packages/language-server/src/core/config/ConfigManager.ts @@ -3,9 +3,9 @@ import { VSCodeEmmetConfig } from '@vscode/emmet-helper'; import { LSConfig, LSCSSConfig, LSHTMLConfig, LSTypescriptConfig } from './interfaces'; import { Connection, DidChangeConfigurationParams } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { FormatCodeSettings, TsConfigSourceFile, UserPreferences } from 'typescript'; +import { FormatCodeSettings, SemicolonPreference, TsConfigSourceFile, UserPreferences } from 'typescript'; -const defaultLSConfig: LSConfig = { +export const defaultLSConfig: LSConfig = { typescript: { enabled: true, diagnostics: { enabled: true }, @@ -46,14 +46,14 @@ type DeepPartial = T extends Record * For more info on this, see the [internal docs](../../../../../docs/internal/language-server/config.md) */ export class ConfigManager { - private config = defaultLSConfig; + private globalConfig: Record = { astro: defaultLSConfig }; private documentSettings: Record>> = {}; private isTrusted = true; - private connection: Connection; + private connection: Connection | undefined; - constructor(connection: Connection) { + constructor(connection?: Connection) { this.connection = connection; } @@ -66,7 +66,11 @@ export class ConfigManager { delete this.documentSettings[scopeUri]; } - async getConfig(section: string, scopeUri: string): Promise> { + async getConfig(section: string, scopeUri: string): Promise { + if (!this.connection) { + return this.globalConfig[section]; + } + if (!this.documentSettings[scopeUri]) { this.documentSettings[scopeUri] = {}; } @@ -90,12 +94,40 @@ export class ConfigManager { async getTSFormatConfig(document: TextDocument): Promise { const formatConfig = (await this.getConfig('typescript.format', document.uri)) ?? {}; - return formatConfig; + return { + // We can use \n here since the editor normalizes later on to its line endings. + newLineCharacter: '\n', + insertSpaceAfterCommaDelimiter: formatConfig.insertSpaceAfterCommaDelimiter ?? true, + insertSpaceAfterConstructor: formatConfig.insertSpaceAfterConstructor ?? false, + insertSpaceAfterSemicolonInForStatements: formatConfig.insertSpaceAfterSemicolonInForStatements ?? true, + insertSpaceBeforeAndAfterBinaryOperators: formatConfig.insertSpaceBeforeAndAfterBinaryOperators ?? true, + insertSpaceAfterKeywordsInControlFlowStatements: + formatConfig.insertSpaceAfterKeywordsInControlFlowStatements ?? true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: + formatConfig.insertSpaceAfterFunctionKeywordForAnonymousFunctions ?? true, + insertSpaceBeforeFunctionParenthesis: formatConfig.insertSpaceBeforeFunctionParenthesis ?? false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: + formatConfig.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis ?? false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: + formatConfig.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets ?? false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: + formatConfig.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces ?? true, + insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: + formatConfig.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces ?? true, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: + formatConfig.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces ?? false, + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: + formatConfig.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces ?? false, + insertSpaceAfterTypeAssertion: formatConfig.insertSpaceAfterTypeAssertion ?? false, + placeOpenBraceOnNewLineForFunctions: formatConfig.placeOpenBraceOnNewLineForFunctions ?? false, + placeOpenBraceOnNewLineForControlBlocks: formatConfig.placeOpenBraceOnNewLineForControlBlocks ?? false, + semicolons: formatConfig.semicolons ?? SemicolonPreference.Ignore, + }; } async getTSPreferences(document: TextDocument): Promise { const config = (await this.getConfig('typescript', document.uri)) ?? {}; - const preferences = config['preferences']; + const preferences = (await this.getConfig('typescript.preferences', document.uri)) ?? {}; return { quotePreference: getQuoteStylePreference(preferences), @@ -109,33 +141,29 @@ export class ConfigManager { includeCompletionsWithSnippetText: config.suggest?.includeCompletionsWithSnippetText ?? true, includeCompletionsForModuleExports: config.suggest?.autoImports ?? true, allowIncompleteCompletions: true, + includeCompletionsWithInsertText: true, }; } /** - * Whether or not specified setting is enabled - * @param key a string which is a path. Example: 'astro.diagnostics.enabled'. + * Return true if a plugin and an optional feature is enabled */ - enabled(key: string): boolean { - return !!this.get(key); - } - async isEnabled( document: TextDocument, plugin: keyof LSConfig, feature?: keyof LSTypescriptConfig | keyof LSCSSConfig | keyof LSHTMLConfig - ) { + ): Promise { const config = await this.getConfig('astro', document.uri); - return feature ? config[plugin][feature]['enabled'] : config[plugin]['enabled']; + return feature ? config[plugin].enabled && config[plugin][feature].enabled : config[plugin].enabled; } /** - * Get a specific setting value - * @param key a string which is a path. Example: 'astro.diagnostics.enable'. + * Updating the global config should only be done in cases where the client doesn't support `workspace/configuration` + * or inside of tests */ - get(key: string): T { - return get(this.config, key); + updateGlobalConfig(config: DeepPartial) { + this.globalConfig.astro = merge({}, defaultLSConfig, this.globalConfig.astro, config); } } diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index c09d2aa5..acd3bb83 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -76,7 +76,7 @@ export class TypeScriptPlugin implements Plugin { } async doHover(document: AstroDocument, position: Position): Promise { - if (!this.featureEnabled('hover')) { + if (!(await this.featureEnabled(document, 'hover'))) { return null; } @@ -118,19 +118,19 @@ export class TypeScriptPlugin implements Plugin { } async getSemanticTokens( - textDocument: AstroDocument, + document: AstroDocument, range?: Range, cancellationToken?: CancellationToken ): Promise { - if (!this.featureEnabled('semanticTokens')) { + if (!(await this.featureEnabled(document, 'semanticTokens'))) { return null; } - return this.semanticTokensProvider.getSemanticTokens(textDocument, range, cancellationToken); + return this.semanticTokensProvider.getSemanticTokens(document, range, cancellationToken); } async getDocumentSymbols(document: AstroDocument): Promise { - if (!this.featureEnabled('documentSymbols')) { + if (!(await this.featureEnabled(document, 'documentSymbols'))) { return []; } @@ -145,7 +145,7 @@ export class TypeScriptPlugin implements Plugin { context: CodeActionContext, cancellationToken?: CancellationToken ): Promise { - if (!this.featureEnabled('codeActions')) { + if (!(await this.featureEnabled(document, 'codeActions'))) { return []; } @@ -158,7 +158,7 @@ export class TypeScriptPlugin implements Plugin { completionContext?: CompletionContext, cancellationToken?: CancellationToken ): Promise | null> { - if (!this.featureEnabled('completions')) { + if (!(await this.featureEnabled(document, 'completions'))) { return null; } @@ -228,7 +228,7 @@ export class TypeScriptPlugin implements Plugin { } async getDiagnostics(document: AstroDocument, cancellationToken?: CancellationToken): Promise { - if (!this.featureEnabled('diagnostics')) { + if (!(await this.featureEnabled(document, 'diagnostics'))) { return []; } @@ -315,9 +315,10 @@ export class TypeScriptPlugin implements Plugin { } } - private featureEnabled(feature: keyof LSTypescriptConfig) { + private async featureEnabled(document: AstroDocument, feature: keyof LSTypescriptConfig) { return ( - this.configManager.enabled('typescript.enabled') && this.configManager.enabled(`typescript.${feature}.enabled`) + (await this.configManager.isEnabled(document, 'typescript')) && + (await this.configManager.isEnabled(document, 'typescript', feature)) ); } } diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 15da4106..fe9f7935 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -8,7 +8,7 @@ import { ShowMessageNotification, TextDocumentIdentifier, } from 'vscode-languageserver'; -import { ConfigManager } from './core/config/ConfigManager'; +import { ConfigManager, defaultLSConfig } from './core/config/ConfigManager'; import { DocumentManager } from './core/documents/DocumentManager'; import { DiagnosticsManager } from './core/DiagnosticsManager'; import { AstroPlugin } from './plugins/astro/AstroPlugin'; @@ -21,6 +21,7 @@ import { debounceThrottle, getUserAstroVersion, urlToPath } from './utils'; import { AstroDocument } from './core/documents'; import { getSemanticTokenLegend } from './plugins/typescript/utils'; import { sortImportKind } from './plugins/typescript/features/CodeActionsProvider'; +import { LSConfig } from './core/config'; const TagCloseRequest: vscode.RequestType = new vscode.RequestType('html/tag'); @@ -32,6 +33,8 @@ export function startLanguageServer(connection: vscode.Connection) { const pluginHost = new PluginHost(documentManager); const configManager = new ConfigManager(connection); + let hasConfigurationCapability = false; + connection.onInitialize((params: vscode.InitializeParams) => { const workspaceUris = params.workspaceFolders?.map((folder) => folder.uri.toString()) ?? [params.rootUri ?? '']; @@ -55,6 +58,8 @@ export function startLanguageServer(connection: vscode.Connection) { } }); + hasConfigurationCapability = !!(params.capabilities.workspace && !!params.capabilities.workspace.configuration); + pluginHost.initialize({ filterIncompleteCompletions: !params.initializationOptions?.dontFilterIncompleteCompletions, definitionLinkSupport: !!params.capabilities.textDocument?.definition?.linkSupport, @@ -139,12 +144,16 @@ export function startLanguageServer(connection: vscode.Connection) { // The params don't matter here because in "pull mode" it's always null, it's intended that when the config is updated // you should just reset "your internal cache" and get the config again for relevant documents, weird API design - connection.onDidChangeConfiguration(async () => { - configManager.updateConfig(); - - documentManager.getAllOpenedByClient().forEach(async (document) => { - await configManager.getConfig('astro', document[1].uri); - }); + connection.onDidChangeConfiguration(async (change) => { + if (hasConfigurationCapability) { + configManager.updateConfig(); + + documentManager.getAllOpenedByClient().forEach(async (document) => { + await configManager.getConfig('astro', document[1].uri); + }); + } else { + configManager.updateGlobalConfig(change.settings.astro || defaultLSConfig); + } }); // Documents @@ -250,7 +259,9 @@ export function startLanguageServer(connection: vscode.Connection) { connection.console.log('Successfully initialized! 🚀'); // Register for all configuration changes. - connection.client.register(DidChangeConfigurationNotification.type); + if (hasConfigurationCapability) { + connection.client.register(DidChangeConfigurationNotification.type); + } }); connection.listen(); diff --git a/packages/language-server/test/plugins/css/CSSPlugin.test.ts b/packages/language-server/test/plugins/css/CSSPlugin.test.ts index bf98645f..60a2be0f 100644 --- a/packages/language-server/test/plugins/css/CSSPlugin.test.ts +++ b/packages/language-server/test/plugins/css/CSSPlugin.test.ts @@ -16,10 +16,10 @@ describe('CSS Plugin', () => { } describe('provide completions', () => { - it('in style tags', () => { + it('in style tags', async () => { const { plugin, document } = setup(''); - const completions = plugin.getCompletions(document, Position.create(0, 7), { + const completions = await plugin.getCompletions(document, Position.create(0, 7), { triggerCharacter: '.', } as CompletionContext); @@ -27,13 +27,13 @@ describe('CSS Plugin', () => { expect(completions, 'Expected completions to not be empty').to.not.be.null; }); - it('in multiple style tags', () => { + it('in multiple style tags', async () => { const { plugin, document } = setup(''); - const completions1 = plugin.getCompletions(document, Position.create(0, 7), { + const completions1 = await plugin.getCompletions(document, Position.create(0, 7), { triggerCharacter: '.', } as CompletionContext); - const completions2 = plugin.getCompletions(document, Position.create(0, 22), { + const completions2 = await plugin.getCompletions(document, Position.create(0, 22), { triggerCharacter: '.', } as CompletionContext); @@ -43,19 +43,19 @@ describe('CSS Plugin', () => { expect(completions2, 'Expected completions2 to not be empty').to.not.be.null; }); - it('in style attributes', () => { + it('in style attributes', async () => { const { plugin, document } = setup('
'); - const completions = plugin.getCompletions(document, Position.create(0, 12)); + const completions = await plugin.getCompletions(document, Position.create(0, 12)); expect(completions.items, 'Expected completions to be an array').to.be.an('array'); expect(completions, 'Expected completions to not be empty').to.not.be.null; }); - it('for :global modifier', () => { + it('for :global modifier', async () => { const { plugin, document } = setup(''); - const completions = plugin.getCompletions(document, Position.create(0, 9), { + const completions = await plugin.getCompletions(document, Position.create(0, 9), { triggerCharacter: ':', } as CompletionContext); const globalCompletion = completions?.items.find((item) => item.label === ':global()'); @@ -63,10 +63,10 @@ describe('CSS Plugin', () => { expect(globalCompletion, 'Expected completions to contain :global modifier').to.not.be.null; }); - it('Emmet completions', () => { + it('Emmet completions', async () => { const { plugin, document } = setup(''); - const completions = plugin.getCompletions(document, Position.create(0, 13)); + const completions = await plugin.getCompletions(document, Position.create(0, 13)); const emmetCompletion = completions?.items.find((item) => item.detail === 'Emmet Abbreviation'); expect(emmetCompletion).to.deep.equal({ @@ -82,21 +82,21 @@ describe('CSS Plugin', () => { }); }); - it('should not provide completions for unclosed style tags', () => { + it('should not provide completions for unclosed style tags', async () => { const { plugin, document } = setup(''); // Disable completions - configManager.updateConfig({ + configManager.updateGlobalConfig({ css: { completions: { enabled: false, @@ -104,21 +104,22 @@ describe('CSS Plugin', () => { }, }); - const completions = plugin.getCompletions(document, Position.create(0, 7), { + const completions = await plugin.getCompletions(document, Position.create(0, 7), { triggerCharacter: '.', } as CompletionContext); - expect(configManager.enabled(`css.completions.enabled`), 'Expected completions to be disabled in configManager') - .to.be.false; + const isEnabled = await configManager.isEnabled(document, 'css', 'completions'); + + expect(isEnabled).to.be.false; expect(completions, 'Expected completions to be null').to.be.null; }); }); describe('provide hover info', () => { - it('in style tags', () => { + it('in style tags', async () => { const { plugin, document } = setup(''); - expect(plugin.doHover(document, Position.create(0, 8))).to.deep.equal({ + expect(await plugin.doHover(document, Position.create(0, 8))).to.deep.equal({ contents: [ { language: 'html', value: '

' }, '[Selector Specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity): (0, 0, 1)', @@ -126,7 +127,7 @@ describe('CSS Plugin', () => { range: Range.create(0, 7, 0, 9), }); - expect(plugin.doHover(document, Position.create(0, 12))).to.deep.equal({ + expect(await plugin.doHover(document, Position.create(0, 12))).to.deep.equal({ contents: { kind: 'markdown', value: @@ -136,10 +137,10 @@ describe('CSS Plugin', () => { }); }); - it('in style attributes', () => { + it('in style attributes', async () => { const { plugin, document } = setup('
'); - expect(plugin.doHover(document, Position.create(0, 13))).to.deep.equal({ + expect(await plugin.doHover(document, Position.create(0, 13))).to.deep.equal({ contents: { kind: 'markdown', value: @@ -149,19 +150,19 @@ describe('CSS Plugin', () => { }); }); - it('should not provide hover info for unclosed style tags', () => { + it('should not provide hover info for unclosed style tags', async () => { const { plugin, document } = setup(''); // Disable hover info - configManager.updateConfig({ + configManager.updateGlobalConfig({ css: { hover: { enabled: false, @@ -169,9 +170,11 @@ describe('CSS Plugin', () => { }, }); - const hoverInfo = plugin.doHover(document, Position.create(0, 8)); + const hoverInfo = await plugin.doHover(document, Position.create(0, 8)); + + const isEnabled = await configManager.isEnabled(document, 'css', 'hover'); - expect(configManager.enabled(`css.hover.enabled`), 'Expected hover to be disabled in configManager').to.be.false; + expect(isEnabled).to.be.false; expect(hoverInfo, 'Expected hoverInfo to be null').to.be.null; }); }); @@ -235,10 +238,10 @@ describe('CSS Plugin', () => { }); describe('provides document symbols', () => { - it('for normal CSS', () => { + it('for normal CSS', async () => { const { plugin, document } = setup(''); - const symbols = plugin.getDocumentSymbols(document); + const symbols = await plugin.getDocumentSymbols(document); expect(symbols).to.deep.equal([ { @@ -252,10 +255,10 @@ describe('CSS Plugin', () => { ]); }); - it('for multiple style tags', () => { + it('for multiple style tags', async () => { const { plugin, document } = setup(''); - const symbols = plugin.getDocumentSymbols(document); + const symbols = await plugin.getDocumentSymbols(document); expect(symbols).to.deep.equal([ { @@ -271,11 +274,11 @@ describe('CSS Plugin', () => { ]); }); - it('should not provide document symbols if feature is disabled', () => { + it('should not provide document symbols if feature is disabled', async () => { const { plugin, document, configManager } = setup(''); // Disable documentSymbols - configManager.updateConfig({ + configManager.updateGlobalConfig({ css: { documentSymbols: { enabled: false, @@ -283,21 +286,20 @@ describe('CSS Plugin', () => { }, }); - const symbols = plugin.getDocumentSymbols(document); + const symbols = await plugin.getDocumentSymbols(document); - expect( - configManager.enabled(`css.documentSymbols.enabled`), - 'Expected documentSymbols to be disabled in configManager' - ).to.be.false; + const isEnabled = await configManager.isEnabled(document, 'css', 'documentSymbols'); + + expect(isEnabled).to.be.false; expect(symbols, 'Expected symbols to be empty').to.be.empty; }); }); describe('provide document colors', () => { - it('for normal css', () => { + it('for normal css', async () => { const { plugin, document } = setup(''); // Disable document colors - configManager.updateConfig({ + configManager.updateGlobalConfig({ css: { documentColors: { enabled: false, @@ -348,12 +350,11 @@ describe('CSS Plugin', () => { }, }); - const documentColors = plugin.getDocumentColors(document); + const documentColors = await plugin.getDocumentColors(document); + + const isEnabled = await configManager.isEnabled(document, 'css', 'documentColors'); - expect( - configManager.enabled(`css.documentColors.enabled`), - 'Expected documentColors to be disabled in configManager' - ).to.be.false; + expect(isEnabled).to.be.false; expect(documentColors, 'Expected documentColors to be null').to.be.empty; }); }); diff --git a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts index 7f560969..ce1d555f 100644 --- a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts +++ b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts @@ -15,37 +15,37 @@ describe('HTML Plugin', () => { } describe('provide completions', () => { - it('for normal html', () => { + it('for normal html', async () => { const { plugin, document } = setup('<'); - const completions = plugin.getCompletions(document, Position.create(0, 1)); + const completions = await plugin.getCompletions(document, Position.create(0, 1)); expect(completions.items, 'Expected completions to be an array').to.be.an('array'); expect(completions, 'Expected completions to not be empty').to.not.be.undefined; }); - it('for style lang in style tags', () => { + it('for style lang in style tags', async () => { const { plugin, document } = setup(' item.label === 'style (lang="less")')).to.not.be.undefined; }); - it('should not provide completions inside of an expression', () => { + it('should not provide completions inside of an expression', async () => { const { plugin, document } = setup('
{ + it('should not provide completions if feature is disabled', async () => { const { plugin, document, configManager } = setup('<'); // Disable completions - configManager.updateConfig({ + configManager.updateGlobalConfig({ html: { completions: { enabled: false, @@ -53,19 +53,19 @@ describe('HTML Plugin', () => { }, }); - const completions = plugin.getCompletions(document, Position.create(0, 7)); + const completions = await plugin.getCompletions(document, Position.create(0, 7)); + const isEnabled = await configManager.isEnabled(document, 'html', 'completions'); - expect(configManager.enabled(`html.completions.enabled`), 'Expected completions to be disabled in configManager') - .to.be.false; + expect(isEnabled).to.be.false; expect(completions, 'Expected completions to be null').to.be.null; }); }); describe('provide hover info', () => { - it('for HTML elements', () => { + it('for HTML elements', async () => { const { plugin, document } = setup('

Build fast websites, faster.

'); - expect(plugin.doHover(document, Position.create(0, 1))).to.deep.equal({ + expect(await plugin.doHover(document, Position.create(0, 1))).to.deep.equal({ contents: { kind: 'markdown', value: @@ -76,10 +76,10 @@ describe('HTML Plugin', () => { }); }); - it('for HTML attributes', () => { + it('for HTML attributes', async () => { const { plugin, document } = setup('

Build fast websites, faster.

'); - expect(plugin.doHover(document, Position.create(0, 4))).to.deep.equal({ + expect(await plugin.doHover(document, Position.create(0, 4))).to.deep.equal({ contents: { kind: 'markdown', value: @@ -90,11 +90,11 @@ describe('HTML Plugin', () => { }); }); - it('should not provide hover info if feature is disabled', () => { + it('should not provide hover info if feature is disabled', async () => { const { plugin, document, configManager } = setup('

Build fast websites, faster.

'); // Disable hover info - configManager.updateConfig({ + configManager.updateGlobalConfig({ html: { hover: { enabled: false, @@ -102,9 +102,10 @@ describe('HTML Plugin', () => { }, }); - const hoverInfo = plugin.doHover(document, Position.create(0, 1)); + const hoverInfo = await plugin.doHover(document, Position.create(0, 1)); + const isEnabled = await configManager.isEnabled(document, 'html', 'hover'); - expect(configManager.enabled(`html.hover.enabled`), 'Expected hover to be disabled in configManager').to.be.false; + expect(isEnabled).to.be.false; expect(hoverInfo, 'Expected hoverInfo to be null').to.be.null; }); }); @@ -129,10 +130,10 @@ describe('HTML Plugin', () => { }); describe('provides document symbols', () => { - it('for html', () => { + it('for html', async () => { const { plugin, document } = setup('

Astro

'); - const symbols = plugin.getDocumentSymbols(document); + const symbols = await plugin.getDocumentSymbols(document); expect(symbols).to.deep.equal([ { @@ -150,11 +151,11 @@ describe('HTML Plugin', () => { ]); }); - it('should not provide document symbols if feature is disabled', () => { + it('should not provide document symbols if feature is disabled', async () => { const { plugin, document, configManager } = setup('

Astro

'); // Disable documentSymbols - configManager.updateConfig({ + configManager.updateGlobalConfig({ html: { documentSymbols: { enabled: false, @@ -162,12 +163,10 @@ describe('HTML Plugin', () => { }, }); - const symbols = plugin.getDocumentSymbols(document); + const symbols = await plugin.getDocumentSymbols(document); + const isEnabled = await configManager.isEnabled(document, 'html', 'documentSymbols'); - expect( - configManager.enabled(`html.documentSymbols.enabled`), - 'Expected documentSymbols to be disabled in configManager' - ).to.be.false; + expect(isEnabled).to.be.false; expect(symbols, 'Expected symbols to be empty').to.be.empty; }); }); diff --git a/packages/language-server/test/plugins/typescript/TypeScriptPlugin.test.ts b/packages/language-server/test/plugins/typescript/TypeScriptPlugin.test.ts index 8b762ab4..7fca4afa 100644 --- a/packages/language-server/test/plugins/typescript/TypeScriptPlugin.test.ts +++ b/packages/language-server/test/plugins/typescript/TypeScriptPlugin.test.ts @@ -28,7 +28,7 @@ describe('TypeScript Plugin', () => { it('should not provide documentSymbols if feature is disabled', async () => { const { plugin, document, configManager } = setup('documentSymbols/documentSymbols.astro'); - configManager.updateConfig({ + configManager.updateGlobalConfig({ typescript: { documentSymbols: { enabled: false, @@ -37,8 +37,9 @@ describe('TypeScript Plugin', () => { }); const symbols = await plugin.getDocumentSymbols(document); + const isEnabled = await configManager.isEnabled(document, 'typescript', 'documentSymbols'); - expect(configManager.enabled(`typescript.documentSymbols.enabled`)).to.be.false; + expect(isEnabled).to.be.false; expect(symbols).to.be.empty; }); }); @@ -54,7 +55,7 @@ describe('TypeScript Plugin', () => { it('should not provide hover info if feature is disabled', async () => { const { plugin, document, configManager } = setup('hoverInfo.astro'); - configManager.updateConfig({ + configManager.updateGlobalConfig({ typescript: { hover: { enabled: false, @@ -64,7 +65,9 @@ describe('TypeScript Plugin', () => { const hoverInfo = await plugin.doHover(document, Position.create(1, 10)); - expect(configManager.enabled(`typescript.hover.enabled`)).to.be.false; + const isEnabled = await configManager.isEnabled(document, 'typescript', 'hover'); + + expect(isEnabled).to.be.false; expect(hoverInfo).to.be.null; }); }); @@ -80,7 +83,7 @@ describe('TypeScript Plugin', () => { it('should not provide diagnostics if feature is disabled', async () => { const { plugin, document, configManager } = setup('diagnostics/basic.astro'); - configManager.updateConfig({ + configManager.updateGlobalConfig({ typescript: { diagnostics: { enabled: false, @@ -115,7 +118,7 @@ describe('TypeScript Plugin', () => { it('should not provide code actions if feature is disabled', async () => { const { plugin, document, configManager } = setup('codeActions/basic.astro'); - configManager.updateConfig({ + configManager.updateGlobalConfig({ typescript: { codeActions: { enabled: false, @@ -135,7 +138,9 @@ describe('TypeScript Plugin', () => { only: [CodeActionKind.QuickFix], }); - expect(configManager.enabled(`typescript.codeActions.enabled`)).to.be.false; + const isEnabled = await configManager.isEnabled(document, 'typescript', 'codeActions'); + + expect(isEnabled).to.be.false; expect(codeActions).to.be.empty; }); }); @@ -158,7 +163,7 @@ describe('TypeScript Plugin', () => { it('should not provide semantic tokens if feature is disabled', async () => { const { plugin, document, configManager } = setup('semanticTokens/frontmatter.astro'); - configManager.updateConfig({ + configManager.updateGlobalConfig({ typescript: { semanticTokens: { enabled: false, @@ -167,7 +172,9 @@ describe('TypeScript Plugin', () => { }); const semanticTokens = await plugin.getSemanticTokens(document); - expect(configManager.enabled(`typescript.semanticTokens.enabled`)).to.be.false; + const isEnabled = await configManager.isEnabled(document, 'typescript', 'semanticTokens'); + + expect(isEnabled).to.be.false; expect(semanticTokens).to.be.null; }); }); diff --git a/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts index 88afbd79..68bcdfb0 100644 --- a/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts @@ -11,7 +11,7 @@ describe('TypeScript Plugin#CodeActionsProvider', () => { function setup(filePath: string) { const env = createEnvironment(filePath, 'typescript', 'codeActions'); const languageServiceManager = new LanguageServiceManager(env.docManager, [env.fixturesDir], env.configManager); - const provider = new CodeActionsProviderImpl(languageServiceManager); + const provider = new CodeActionsProviderImpl(languageServiceManager, env.configManager); return { ...env, diff --git a/packages/language-server/test/plugins/typescript/features/CompletionsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionsProvider.test.ts index bcb5c1c3..9a42275f 100644 --- a/packages/language-server/test/plugins/typescript/features/CompletionsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CompletionsProvider.test.ts @@ -12,7 +12,7 @@ describe('TypeScript Plugin#CompletionsProvider', () => { function setup(filePath: string) { const env = createEnvironment(filePath, 'typescript', 'completions'); const languageServiceManager = new LanguageServiceManager(env.docManager, [env.fixturesDir], env.configManager); - const provider = new CompletionsProviderImpl(languageServiceManager); + const provider = new CompletionsProviderImpl(languageServiceManager, env.configManager); return { ...env, @@ -56,7 +56,7 @@ describe('TypeScript Plugin#CompletionsProvider', () => { expect(detail).to.equal('./imports/component.astro'); expect(additionalTextEdits[0].newText).to.equal( - `---${newLine}import Component from './imports/component.astro'${newLine}---${newLine}${newLine}` + `---${newLine}import Component from "./imports/component.astro"${newLine}---${newLine}${newLine}` ); }); @@ -69,7 +69,7 @@ describe('TypeScript Plugin#CompletionsProvider', () => { const { additionalTextEdits, detail } = await provider.resolveCompletion(document, item!); expect(detail).to.equal('./imports/component.astro'); - expect(additionalTextEdits[0].newText).to.equal(`import Component from './imports/component.astro';${newLine}`); + expect(additionalTextEdits[0].newText).to.equal(`import Component from "./imports/component.astro";${newLine}`); }); it('resolve completion without auto import if component import already exists', async () => { @@ -105,7 +105,7 @@ describe('TypeScript Plugin#CompletionsProvider', () => { expect(item).to.deep.equal({ commitCharacters: ['.', ',', '('], - insertText: "import { MySuperFunction$1 } from './imports/definitions';", + insertText: 'import { MySuperFunction$1 } from "./imports/definitions";', insertTextFormat: 2, kind: CompletionItemKind.Function, label: 'MySuperFunction', @@ -115,7 +115,7 @@ describe('TypeScript Plugin#CompletionsProvider', () => { preselect: undefined, sortText: '11', textEdit: { - newText: "import { MySuperFunction$1 } from './imports/definitions';", + newText: 'import { MySuperFunction$1 } from "./imports/definitions";', range: Range.create(1, 1, 1, 10), }, }); From a19719120229efa898118016217b1b49d20e42b5 Mon Sep 17 00:00:00 2001 From: Princesseuh Date: Fri, 29 Apr 2022 13:41:58 -0400 Subject: [PATCH 3/3] Remove unneeded comment --- .../typescript/features/CompletionsProvider.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/features/CompletionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionsProvider.ts index fb98138c..67e64d63 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionsProvider.ts @@ -32,17 +32,6 @@ import { getMarkdownDocumentation } from '../previewer'; import { isPartOfImportStatement } from './utils'; import { ConfigManager } from '../../../core/config'; -// export const completionOptions: ts.GetCompletionsAtPositionOptions = { -// importModuleSpecifierPreference: 'relative', -// importModuleSpecifierEnding: 'auto', -// quotePreference: 'single', -// includeCompletionsForModuleExports: true, -// includeCompletionsForImportStatements: true, -// includeCompletionsWithInsertText: true, -// allowIncompleteCompletions: true, -// includeCompletionsWithSnippetText: true, -// }; - /** * The language service throws an error if the character is not a valid trigger character. * Also, the completions are worse.