🎉 initiate project *astro_rewrite*

This commit is contained in:
sindrekjelsrud 2023-07-19 21:31:30 +02:00
parent ffd4d5e86c
commit 2ba37bfbe3
8658 changed files with 2268794 additions and 2538 deletions

37
node_modules/@astrojs/language-server/LICENSE generated vendored Normal file
View file

@ -0,0 +1,37 @@
MIT License
Copyright (c) 2021 Nate Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
This license applies to code originating from the https://github.com/sveltejs/language-tools repository,
which has provided an extremely solid foundation for us to build upon:
Copyright (c) 2020-Present [these people](https://github.com/sveltejs/language-tools/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

15
node_modules/@astrojs/language-server/README.md generated vendored Normal file
View file

@ -0,0 +1,15 @@
# @astrojs/language-server
The Astro language server, implement the [language server protocol](https://microsoft.github.io/language-server-protocol/)
## Folder structure
```plaintext
├── bin # .js file used to start the server
├── dist # Compiled files, generated by TypeScript
├── src # Source files
│ ├── core # Core code such as .astro file parsing, configuration manager, document definition etc
│ └── plugins # Modules for the different languages supported in .astro files
├── test # Tests
└── types # Types injected into Astro files by the language server under certain conditions
```

View file

@ -0,0 +1,3 @@
#! /usr/bin/env node
require('../dist/browser.js');

3
node_modules/@astrojs/language-server/bin/nodeServer.js generated vendored Executable file
View file

@ -0,0 +1,3 @@
#! /usr/bin/env node
require('../dist/node.js');

View file

@ -0,0 +1 @@
export {};

15
node_modules/@astrojs/language-server/dist/browser.js generated vendored Normal file
View file

@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const browser_1 = require("vscode-languageserver/browser");
const server_1 = require("./server");
const messageReader = new browser_1.BrowserMessageReader(self);
const messageWriter = new browser_1.BrowserMessageWriter(self);
const connection = (0, browser_1.createConnection)(messageReader, messageWriter);
(0, server_1.startLanguageServer)(connection, {
loadTypescript(options) {
return undefined; // TODO: Full browser support
},
loadTypescriptLocalized(options) {
return undefined;
},
});

23
node_modules/@astrojs/language-server/dist/check.d.ts generated vendored Normal file
View file

@ -0,0 +1,23 @@
import type { Diagnostic } from 'vscode-languageserver-types';
import { LSConfig } from './core/config';
export { DiagnosticSeverity } from 'vscode-languageserver-types';
export { Diagnostic };
export interface GetDiagnosticsResult {
fileUri: string;
text: string;
diagnostics: Diagnostic[];
}
export declare class AstroCheck {
private docManager;
private configManager;
private pluginHost;
constructor(workspacePath: string, typescriptPath: string, options?: LSConfig);
upsertDocument(doc: {
text: string;
uri: string;
}): void;
removeDocument(uri: string): void;
getDiagnostics(): Promise<GetDiagnosticsResult[]>;
private initialize;
private getDiagnosticsForFile;
}

60
node_modules/@astrojs/language-server/dist/check.js generated vendored Normal file
View file

@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AstroCheck = exports.DiagnosticSeverity = void 0;
const config_1 = require("./core/config");
const documents_1 = require("./core/documents");
const plugins_1 = require("./plugins");
const LanguageServiceManager_1 = require("./plugins/typescript/LanguageServiceManager");
const utils_1 = require("./utils");
var vscode_languageserver_types_1 = require("vscode-languageserver-types");
Object.defineProperty(exports, "DiagnosticSeverity", { enumerable: true, get: function () { return vscode_languageserver_types_1.DiagnosticSeverity; } });
class AstroCheck {
constructor(workspacePath, typescriptPath, options) {
this.docManager = documents_1.DocumentManager.newInstance();
this.configManager = new config_1.ConfigManager();
this.pluginHost = new plugins_1.PluginHost(this.docManager);
try {
const ts = require(typescriptPath);
this.initialize(workspacePath, ts);
}
catch (e) {
throw new Error(`Couldn't load TypeScript from path ${typescriptPath}`);
}
if (options) {
this.configManager.updateGlobalConfig(options);
}
}
upsertDocument(doc) {
this.docManager.openDocument({
text: doc.text,
uri: doc.uri,
});
this.docManager.markAsOpenedInClient(doc.uri);
}
removeDocument(uri) {
if (!this.docManager.get(uri)) {
return;
}
this.docManager.closeDocument(uri);
this.docManager.releaseDocument(uri);
}
async getDiagnostics() {
return await Promise.all(this.docManager.getAllOpenedByClient().map(async (doc) => {
const uri = doc[1].uri;
return await this.getDiagnosticsForFile(uri);
}));
}
initialize(workspacePath, ts) {
const languageServiceManager = new LanguageServiceManager_1.LanguageServiceManager(this.docManager, [(0, utils_1.normalizeUri)(workspacePath)], this.configManager, ts);
this.pluginHost.registerPlugin(new plugins_1.TypeScriptPlugin(this.configManager, languageServiceManager));
}
async getDiagnosticsForFile(uri) {
const diagnostics = await this.pluginHost.getDiagnostics({ uri });
return {
fileUri: uri || '',
text: this.docManager.get(uri)?.getText() || '',
diagnostics,
};
}
}
exports.AstroCheck = AstroCheck;

View file

@ -0,0 +1,13 @@
import type { Connection, Diagnostic, TextDocumentIdentifier } from 'vscode-languageserver';
import type { AstroDocument, DocumentManager } from './documents';
export type SendDiagnostics = Connection['sendDiagnostics'];
export type GetDiagnostics = (doc: TextDocumentIdentifier) => Thenable<Diagnostic[]>;
export declare class DiagnosticsManager {
private sendDiagnostics;
private docManager;
private getDiagnostics;
constructor(sendDiagnostics: SendDiagnostics, docManager: DocumentManager, getDiagnostics: GetDiagnostics);
updateAll(): void;
update(document: AstroDocument): Promise<void>;
removeDiagnostics(document: AstroDocument): void;
}

View file

@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticsManager = void 0;
class DiagnosticsManager {
constructor(sendDiagnostics, docManager, getDiagnostics) {
this.sendDiagnostics = sendDiagnostics;
this.docManager = docManager;
this.getDiagnostics = getDiagnostics;
}
updateAll() {
this.docManager.getAllOpenedByClient().forEach((doc) => {
this.update(doc[1]);
});
}
async update(document) {
const diagnostics = await this.getDiagnostics({ uri: document.getURL() });
this.sendDiagnostics({
uri: document.getURL(),
diagnostics,
});
}
removeDiagnostics(document) {
this.sendDiagnostics({
uri: document.getURL(),
diagnostics: [],
});
}
}
exports.DiagnosticsManager = DiagnosticsManager;

View file

@ -0,0 +1,43 @@
import type { VSCodeEmmetConfig } from '@vscode/emmet-helper';
import type { FormatCodeSettings, UserPreferences } from 'typescript';
import type { Connection, FormattingOptions } from 'vscode-languageserver';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import type { LSConfig, LSCSSConfig, LSHTMLConfig, LSTypescriptConfig } from './interfaces';
export declare const defaultLSConfig: LSConfig;
type DeepPartial<T> = T extends Record<string, unknown> ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
/**
* Manager class to facilitate accessing and updating the user's config
* Not to be confused with other kind of configurations (such as the Astro project configuration and the TypeScript/Javascript one)
* For more info on this, see the [internal docs](../../../../../docs/internal/language-server/config.md)
*/
export declare class ConfigManager {
private connection?;
private hasConfigurationCapability?;
private globalConfig;
private documentSettings;
shouldRefreshTSServices: boolean;
private isTrusted;
constructor(connection?: Connection | undefined, hasConfigurationCapability?: boolean | undefined);
updateConfig(): void;
removeDocument(scopeUri: string): void;
getConfig<T>(section: string, scopeUri: string): Promise<T | Record<string, any>>;
getEmmetConfig(document: TextDocument): Promise<VSCodeEmmetConfig>;
getPrettierVSConfig(document: TextDocument): Promise<Record<string, any>>;
getTSFormatConfig(document: TextDocument, vscodeOptions?: FormattingOptions): Promise<FormatCodeSettings>;
getTSPreferences(document: TextDocument): Promise<UserPreferences>;
/**
* Return true if a plugin and an optional feature is enabled
*/
isEnabled(document: TextDocument, plugin: keyof LSConfig, feature?: keyof LSTypescriptConfig | keyof LSCSSConfig | keyof LSHTMLConfig): Promise<boolean>;
/**
* Updating the global config should only be done in cases where the client doesn't support `workspace/configuration`
* or inside of tests.
*
* The `outsideAstro` parameter can be set to true to change configurations in the global scope.
* For example, to change TypeScript settings
*/
updateGlobalConfig(config: DeepPartial<LSConfig> | any, outsideAstro?: boolean): void;
}
export {};

View file

@ -0,0 +1,226 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigManager = exports.defaultLSConfig = void 0;
const utils_1 = require("../../utils");
// The default language server configuration is used only in two cases:
// 1. When the client does not support `workspace/configuration` requests and as such, needs a global config
// 2. Inside tests, where we don't have a client connection because.. well.. we don't have a client
// Additionally, the default config is used to set default settings for some settings (ex: formatting settings)
exports.defaultLSConfig = {
typescript: {
enabled: true,
allowArbitraryAttributes: false,
diagnostics: { enabled: true },
hover: { enabled: true },
completions: { enabled: true },
definitions: { enabled: true },
documentSymbols: { enabled: true },
codeActions: { enabled: true },
rename: { enabled: true },
signatureHelp: { enabled: true },
semanticTokens: { enabled: true },
},
css: {
enabled: true,
hover: { enabled: true },
completions: { enabled: true, emmet: true },
documentColors: { enabled: true },
documentSymbols: { enabled: true },
},
html: {
enabled: true,
hover: { enabled: true },
completions: { enabled: true, emmet: true },
tagComplete: { enabled: true },
documentSymbols: { enabled: true },
},
format: {
indentFrontmatter: false,
newLineAfterFrontmatter: true,
},
};
/**
* Manager class to facilitate accessing and updating the user's config
* Not to be confused with other kind of configurations (such as the Astro project configuration and the TypeScript/Javascript one)
* For more info on this, see the [internal docs](../../../../../docs/internal/language-server/config.md)
*/
class ConfigManager {
constructor(connection, hasConfigurationCapability) {
this.connection = connection;
this.hasConfigurationCapability = hasConfigurationCapability;
this.globalConfig = { astro: exports.defaultLSConfig };
this.documentSettings = {};
// If set to true, the next time we need a TypeScript language service, we'll rebuild it so it gets the new config
this.shouldRefreshTSServices = false;
this.isTrusted = true;
}
updateConfig() {
// Reset all cached document settings
this.documentSettings = {};
this.shouldRefreshTSServices = true;
}
removeDocument(scopeUri) {
delete this.documentSettings[scopeUri];
}
async getConfig(section, scopeUri) {
if (!this.connection || !this.hasConfigurationCapability) {
return (0, utils_1.get)(this.globalConfig, section) ?? {};
}
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];
}
async getEmmetConfig(document) {
const emmetConfig = (await this.getConfig('emmet', document.uri)) ?? {};
return {
...emmetConfig,
preferences: emmetConfig.preferences ?? {},
showExpandedAbbreviation: emmetConfig.showExpandedAbbreviation ?? 'always',
showAbbreviationSuggestions: emmetConfig.showAbbreviationSuggestions ?? true,
syntaxProfiles: emmetConfig.syntaxProfiles ?? {},
variables: emmetConfig.variables ?? {},
excludeLanguages: emmetConfig.excludeLanguages ?? [],
showSuggestionsAsSnippets: emmetConfig.showSuggestionsAsSnippets ?? false,
};
}
async getPrettierVSConfig(document) {
const prettierVSConfig = (await this.getConfig('prettier', document.uri)) ?? {};
return prettierVSConfig;
}
async getTSFormatConfig(document, vscodeOptions) {
const formatConfig = (await this.getConfig('typescript.format', document.uri)) ?? {};
return {
tabSize: vscodeOptions?.tabSize,
indentSize: vscodeOptions?.tabSize,
convertTabsToSpaces: vscodeOptions?.insertSpaces,
// 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 ?? 'ignore',
};
}
async getTSPreferences(document) {
const config = (await this.getConfig('typescript', document.uri)) ?? {};
const preferences = (await this.getConfig('typescript.preferences', document.uri)) ?? {};
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,
includeCompletionsWithInsertText: true,
// Inlay Hints
includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config),
includeInlayParameterNameHintsWhenArgumentMatchesName: !(config.inlayHints?.parameterNames?.suppressWhenArgumentMatchesName ?? true),
includeInlayFunctionParameterTypeHints: config.inlayHints?.parameterTypes?.enabled ?? false,
includeInlayVariableTypeHints: config.inlayHints?.variableTypes?.enabled ?? false,
includeInlayPropertyDeclarationTypeHints: config.inlayHints?.propertyDeclarationTypes?.enabled ?? false,
includeInlayFunctionLikeReturnTypeHints: config.inlayHints?.functionLikeReturnTypes?.enabled ?? false,
includeInlayEnumMemberValueHints: config.inlayHints?.enumMemberValues?.enabled ?? false,
};
}
/**
* Return true if a plugin and an optional feature is enabled
*/
async isEnabled(document, plugin, feature) {
const config = (await this.getConfig('astro', document.uri)) ?? {};
if (config[plugin]) {
let res = config[plugin].enabled ?? true;
if (feature && config[plugin][feature]) {
res = (res && config[plugin][feature].enabled) ?? true;
}
return res;
}
return true;
}
/**
* Updating the global config should only be done in cases where the client doesn't support `workspace/configuration`
* or inside of tests.
*
* The `outsideAstro` parameter can be set to true to change configurations in the global scope.
* For example, to change TypeScript settings
*/
updateGlobalConfig(config, outsideAstro) {
if (outsideAstro) {
this.globalConfig = (0, utils_1.mergeDeep)({}, this.globalConfig, config);
}
else {
this.globalConfig.astro = (0, utils_1.mergeDeep)({}, exports.defaultLSConfig, this.globalConfig.astro, config);
}
this.shouldRefreshTSServices = true;
}
}
exports.ConfigManager = ConfigManager;
function getQuoteStylePreference(config) {
switch (config.quoteStyle) {
case 'single':
return 'single';
case 'double':
return 'double';
default:
return 'auto';
}
}
function getImportModuleSpecifierPreference(config) {
switch (config.importModuleSpecifier) {
case 'project-relative':
return 'project-relative';
case 'relative':
return 'relative';
case 'non-relative':
return 'non-relative';
default:
return undefined;
}
}
function getImportModuleSpecifierEndingPreference(config) {
switch (config.importModuleSpecifierEnding) {
case 'minimal':
return 'minimal';
case 'index':
return 'index';
case 'js':
return 'js';
default:
return 'auto';
}
}
function getInlayParameterNameHintsPreference(config) {
switch (config.inlayHints?.parameterNames?.enabled) {
case 'none':
return 'none';
case 'literals':
return 'literals';
case 'all':
return 'all';
default:
return undefined;
}
}

View file

@ -0,0 +1,2 @@
export * from './ConfigManager';
export * from './interfaces';

View file

@ -0,0 +1,18 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./ConfigManager"), exports);
__exportStar(require("./interfaces"), exports);

View file

@ -0,0 +1,77 @@
/**
* Representation of the language server config.
* Make sure that this is kept in sync with the `package.json` of the VS Code extension
*/
export interface LSConfig {
typescript?: LSTypescriptConfig;
html?: LSHTMLConfig;
css?: LSCSSConfig;
format?: LSFormatConfig;
}
export interface LSFormatConfig {
indentFrontmatter?: boolean;
newLineAfterFrontmatter?: boolean;
}
export interface LSTypescriptConfig {
enabled?: boolean;
allowArbitraryAttributes?: boolean;
diagnostics?: {
enabled?: boolean;
};
hover?: {
enabled?: boolean;
};
documentSymbols?: {
enabled?: boolean;
};
completions?: {
enabled?: boolean;
};
definitions?: {
enabled?: boolean;
};
codeActions?: {
enabled?: boolean;
};
rename?: {
enabled?: boolean;
};
signatureHelp?: {
enabled?: boolean;
};
semanticTokens?: {
enabled?: boolean;
};
}
export interface LSHTMLConfig {
enabled?: boolean;
hover?: {
enabled?: boolean;
};
completions?: {
enabled?: boolean;
emmet?: boolean;
};
tagComplete?: {
enabled?: boolean;
};
documentSymbols?: {
enabled?: boolean;
};
}
export interface LSCSSConfig {
enabled?: boolean;
hover?: {
enabled?: boolean;
};
completions?: {
enabled?: boolean;
emmet?: boolean;
};
documentColors?: {
enabled?: boolean;
};
documentSymbols?: {
enabled?: boolean;
};
}

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,19 @@
import type { HTMLDocument, Range } from 'vscode-html-languageservice';
import { WritableDocument } from './DocumentBase';
import { AstroMetadata } from './parseAstro';
import { TagInformation } from './utils';
export declare class AstroDocument extends WritableDocument {
url: string;
content: string;
languageId: string;
astroMeta: AstroMetadata;
html: HTMLDocument;
styleTags: TagInformation[];
scriptTags: TagInformation[];
constructor(url: string, content: string);
private updateDocInfo;
setText(text: string): void;
getText(range?: Range | undefined): string;
getURL(): string;
getFilePath(): string | null;
}

View file

@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AstroDocument = void 0;
const utils_1 = require("../../utils");
const DocumentBase_1 = require("./DocumentBase");
const parseAstro_1 = require("./parseAstro");
const parseHtml_1 = require("./parseHtml");
const utils_2 = require("./utils");
class AstroDocument extends DocumentBase_1.WritableDocument {
constructor(url, content) {
super();
this.url = url;
this.content = content;
this.languageId = 'astro';
this.updateDocInfo();
}
updateDocInfo() {
this.astroMeta = (0, parseAstro_1.parseAstro)(this.content);
this.html = (0, parseHtml_1.parseHtml)(this.content, this.astroMeta);
this.styleTags = (0, utils_2.extractStyleTags)(this.content, this.html);
this.scriptTags = (0, utils_2.extractScriptTags)(this.content, this.html);
}
setText(text) {
this.content = text;
this.version++;
this.updateDocInfo();
}
getText(range) {
if (range) {
const start = this.offsetAt(range.start);
const end = this.offsetAt(range.end);
return this.content.substring(start, end);
}
return this.content;
}
getURL() {
return this.url;
}
getFilePath() {
return (0, utils_1.urlToPath)(this.url);
}
}
exports.AstroDocument = AstroDocument;

View file

@ -0,0 +1,68 @@
import type { Position } from 'vscode-languageserver';
import type { TextDocument } from 'vscode-languageserver-textdocument';
/**
* Represents a textual document.
*/
export declare abstract class ReadableDocument implements TextDocument {
/**
* Get the text content of the document
*/
abstract getText(): string;
/**
* Returns the url of the document
*/
abstract getURL(): string;
/**
* Returns the file path if the url scheme is file
*/
abstract getFilePath(): string | null;
/**
* Current version of the document.
*/
version: number;
/**
* Should be cleared when there's an update to the text
*/
protected lineOffsets?: number[];
/**
* Get the length of the document's content
*/
getTextLength(): number;
/**
* Get the line and character based on the offset
* @param offset The index of the position
*/
positionAt(offset: number): Position;
/**
* Get the index of the line and character position
* @param position Line and character position
*/
offsetAt(position: Position): number;
getLineUntilOffset(offset: number): string;
private getLineOffsets;
/**
* Implements TextDocument
*/
get uri(): string;
get lines(): string[];
get lineCount(): number;
abstract languageId: string;
}
/**
* Represents a textual document that can be manipulated.
*/
export declare abstract class WritableDocument extends ReadableDocument {
/**
* Set the text content of the document.
* Implementers should set `lineOffsets` to `undefined` here.
* @param text The new text content
*/
abstract setText(text: string): void;
/**
* Update the text between two positions.
* @param text The new text slice
* @param start Start offset of the new text
* @param end End offset of the new text
*/
update(text: string, start: number, end: number): void;
}

View file

@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WritableDocument = exports.ReadableDocument = void 0;
const utils_1 = require("./utils");
/**
* Represents a textual document.
*/
class ReadableDocument {
constructor() {
/**
* Current version of the document.
*/
this.version = 0;
}
/**
* Get the length of the document's content
*/
getTextLength() {
return this.getText().length;
}
/**
* Get the line and character based on the offset
* @param offset The index of the position
*/
positionAt(offset) {
return (0, utils_1.positionAt)(offset, this.getText(), this.getLineOffsets());
}
/**
* Get the index of the line and character position
* @param position Line and character position
*/
offsetAt(position) {
return (0, utils_1.offsetAt)(position, this.getText(), this.getLineOffsets());
}
getLineUntilOffset(offset) {
const { line, character } = this.positionAt(offset);
return this.lines[line].slice(0, character);
}
getLineOffsets() {
if (!this.lineOffsets) {
this.lineOffsets = (0, utils_1.getLineOffsets)(this.getText());
}
return this.lineOffsets;
}
/**
* Implements TextDocument
*/
get uri() {
return this.getURL();
}
get lines() {
return this.getText().split(/\r?\n/);
}
get lineCount() {
return this.lines.length;
}
}
exports.ReadableDocument = ReadableDocument;
/**
* Represents a textual document that can be manipulated.
*/
class WritableDocument extends ReadableDocument {
/**
* Update the text between two positions.
* @param text The new text slice
* @param start Start offset of the new text
* @param end End offset of the new text
*/
update(text, start, end) {
this.lineOffsets = undefined;
const content = this.getText();
this.setText(content.slice(0, start) + text + content.slice(end));
}
}
exports.WritableDocument = WritableDocument;

View file

@ -0,0 +1,23 @@
import type { TextDocumentContentChangeEvent, TextDocumentItem, VersionedTextDocumentIdentifier } from 'vscode-languageserver';
import { AstroDocument } from './AstroDocument';
export type DocumentEvent = 'documentOpen' | 'documentChange' | 'documentClose';
export declare class DocumentManager {
private createDocument?;
private emitter;
private openedInClient;
private documents;
private locked;
private deleteCandidates;
constructor(createDocument?: ((textDocument: Pick<TextDocumentItem, 'text' | 'uri'>) => AstroDocument) | undefined);
openDocument(textDocument: Pick<TextDocumentItem, 'text' | 'uri'>): AstroDocument;
lockDocument(uri: string): void;
markAsOpenedInClient(uri: string): void;
getAllOpenedByClient(): [string, AstroDocument][];
releaseDocument(uri: string): void;
closeDocument(uri: string): void;
updateDocument(textDocument: VersionedTextDocumentIdentifier, changes: TextDocumentContentChangeEvent[]): void;
on(name: DocumentEvent, listener: (document: AstroDocument) => void): void;
get(uri: string): AstroDocument | undefined;
private notify;
static newInstance(): DocumentManager;
}

View file

@ -0,0 +1,100 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DocumentManager = void 0;
const events_1 = require("events");
const utils_1 = require("../../utils");
const AstroDocument_1 = require("./AstroDocument");
class DocumentManager {
constructor(createDocument) {
this.createDocument = createDocument;
this.emitter = new events_1.EventEmitter();
this.openedInClient = new Set();
this.documents = new Map();
this.locked = new Set();
this.deleteCandidates = new Set();
if (!createDocument) {
this.createDocument = (textDocument) => new AstroDocument_1.AstroDocument(textDocument.uri, textDocument.text);
}
}
openDocument(textDocument) {
textDocument = { ...textDocument, uri: (0, utils_1.normalizeUri)(textDocument.uri) };
let document;
if (this.documents.has(textDocument.uri)) {
document = this.documents.get(textDocument.uri);
document.setText(textDocument.text);
}
else {
document = this.createDocument(textDocument);
this.documents.set(textDocument.uri, document);
this.notify('documentOpen', document);
}
this.notify('documentChange', document);
return document;
}
lockDocument(uri) {
this.locked.add((0, utils_1.normalizeUri)(uri));
}
markAsOpenedInClient(uri) {
this.openedInClient.add((0, utils_1.normalizeUri)(uri));
}
getAllOpenedByClient() {
return Array.from(this.documents.entries()).filter((doc) => this.openedInClient.has(doc[0]));
}
releaseDocument(uri) {
uri = (0, utils_1.normalizeUri)(uri);
this.locked.delete(uri);
this.openedInClient.delete(uri);
if (this.deleteCandidates.has(uri)) {
this.deleteCandidates.delete(uri);
this.closeDocument(uri);
}
}
closeDocument(uri) {
uri = (0, utils_1.normalizeUri)(uri);
const document = this.documents.get(uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
this.notify('documentClose', document);
// Some plugin may prevent a document from actually being closed.
if (!this.locked.has(uri)) {
this.documents.delete(uri);
}
else {
this.deleteCandidates.add(uri);
}
this.openedInClient.delete(uri);
}
updateDocument(textDocument, changes) {
const document = this.documents.get((0, utils_1.normalizeUri)(textDocument.uri));
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
for (const change of changes) {
let start = 0;
let end = 0;
if ('range' in change) {
start = document.offsetAt(change.range.start);
end = document.offsetAt(change.range.end);
}
else {
end = document.getTextLength();
}
document.update(change.text, start, end);
}
this.notify('documentChange', document);
}
on(name, listener) {
this.emitter.on(name, listener);
}
get(uri) {
return this.documents.get((0, utils_1.normalizeUri)(uri));
}
notify(name, document) {
this.emitter.emit(name, document);
}
static newInstance() {
return new DocumentManager(({ uri, text }) => new AstroDocument_1.AstroDocument(uri, text));
}
}
exports.DocumentManager = DocumentManager;

View file

@ -0,0 +1,94 @@
import { TraceMap } from '@jridgewell/trace-mapping';
import type ts from 'typescript';
import { CodeAction, ColorPresentation, CompletionItem, Diagnostic, FoldingRange, Hover, InsertReplaceEdit, LocationLink, Position, Range, SelectionRange, SymbolInformation, TextDocumentEdit, TextEdit } from 'vscode-languageserver';
import { DocumentSnapshot, ScriptTagDocumentSnapshot } from '../../plugins/typescript/snapshots/DocumentSnapshot';
import { TagInformation } from './utils';
export interface DocumentMapper {
/**
* Map the generated position to the original position
* @param generatedPosition Position in fragment
*/
getOriginalPosition(generatedPosition: Position): Position;
/**
* Map the original position to the generated position
* @param originalPosition Position in parent
*/
getGeneratedPosition(originalPosition: Position): Position;
/**
* Returns true if the given original position is inside of the generated map
* @param pos Position in original
*/
isInGenerated(pos: Position): boolean;
/**
* Get document URL
*/
getURL(): string;
/**
* Implement this if you need teardown logic before this mapper gets cleaned up.
*/
destroy?(): void;
}
/**
* Does not map, returns positions as is.
*/
export declare class IdentityMapper implements DocumentMapper {
private url;
private parent?;
constructor(url: string, parent?: DocumentMapper | undefined);
getOriginalPosition(generatedPosition: Position): Position;
getGeneratedPosition(originalPosition: Position): Position;
isInGenerated(position: Position): boolean;
getURL(): string;
destroy(): void;
}
/**
* Maps positions in a fragment relative to a parent.
*/
export declare class FragmentMapper implements DocumentMapper {
private originalText;
private tagInfo;
private url;
private lineOffsetsOriginal;
private lineOffsetsGenerated;
constructor(originalText: string, tagInfo: TagInformation, url: string);
getOriginalPosition(generatedPosition: Position): Position;
private offsetInParent;
getGeneratedPosition(originalPosition: Position): Position;
isInGenerated(pos: Position): boolean;
getURL(): string;
}
export declare class SourceMapDocumentMapper implements DocumentMapper {
protected traceMap: TraceMap;
protected sourceUri: string;
private parent?;
constructor(traceMap: TraceMap, sourceUri: string, parent?: DocumentMapper | undefined);
getOriginalPosition(generatedPosition: Position): Position;
getGeneratedPosition(originalPosition: Position): Position;
isInGenerated(position: Position): boolean;
getURL(): string;
}
export declare class ConsumerDocumentMapper extends SourceMapDocumentMapper {
private nrPrependesLines;
constructor(traceMap: TraceMap, sourceUri: string, nrPrependesLines: number);
getOriginalPosition(generatedPosition: Position): Position;
getGeneratedPosition(originalPosition: Position): Position;
isInGenerated(): boolean;
}
export declare function mapRangeToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, range: Range): Range;
export declare function mapRangeToGenerated(fragment: DocumentMapper, range: Range): Range;
export declare function mapCompletionItemToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, item: CompletionItem): CompletionItem;
export declare function mapHoverToParent(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, hover: Hover): Hover;
export declare function mapObjWithRangeToOriginal<T extends {
range: Range;
}>(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, objWithRange: T): T;
export declare function mapInsertReplaceEditToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, edit: InsertReplaceEdit): InsertReplaceEdit;
export declare function mapEditToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, edit: TextEdit | InsertReplaceEdit): TextEdit | InsertReplaceEdit;
export declare function mapDiagnosticToGenerated(fragment: DocumentMapper, diagnostic: Diagnostic): Diagnostic;
export declare function mapColorPresentationToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, presentation: ColorPresentation): ColorPresentation;
export declare function mapSymbolInformationToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, info: SymbolInformation): SymbolInformation;
export declare function mapLocationLinkToOriginal(fragment: DocumentMapper, def: LocationLink): LocationLink;
export declare function mapTextDocumentEditToOriginal(fragment: DocumentMapper, edit: TextDocumentEdit): TextDocumentEdit;
export declare function mapCodeActionToOriginal(fragment: DocumentMapper, codeAction: CodeAction): CodeAction;
export declare function mapScriptSpanStartToSnapshot(span: ts.TextSpan, scriptTagSnapshot: ScriptTagDocumentSnapshot, tsSnapshot: DocumentSnapshot): number;
export declare function mapFoldingRangeToParent(fragment: DocumentMapper, foldingRange: FoldingRange): FoldingRange;
export declare function mapSelectionRangeToParent(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, selectionRange: SelectionRange): SelectionRange;

View file

@ -0,0 +1,264 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.mapSelectionRangeToParent = exports.mapFoldingRangeToParent = exports.mapScriptSpanStartToSnapshot = exports.mapCodeActionToOriginal = exports.mapTextDocumentEditToOriginal = exports.mapLocationLinkToOriginal = exports.mapSymbolInformationToOriginal = exports.mapColorPresentationToOriginal = exports.mapDiagnosticToGenerated = exports.mapEditToOriginal = exports.mapInsertReplaceEditToOriginal = exports.mapObjWithRangeToOriginal = exports.mapHoverToParent = exports.mapCompletionItemToOriginal = exports.mapRangeToGenerated = exports.mapRangeToOriginal = exports.ConsumerDocumentMapper = exports.SourceMapDocumentMapper = exports.FragmentMapper = exports.IdentityMapper = void 0;
const trace_mapping_1 = require("@jridgewell/trace-mapping");
const vscode_languageserver_1 = require("vscode-languageserver");
const utils_1 = require("./utils");
/**
* Does not map, returns positions as is.
*/
class IdentityMapper {
constructor(url, parent) {
this.url = url;
this.parent = parent;
}
getOriginalPosition(generatedPosition) {
if (this.parent) {
generatedPosition = this.getOriginalPosition(generatedPosition);
}
return generatedPosition;
}
getGeneratedPosition(originalPosition) {
if (this.parent) {
originalPosition = this.getGeneratedPosition(originalPosition);
}
return originalPosition;
}
isInGenerated(position) {
if (this.parent && !this.parent.isInGenerated(position)) {
return false;
}
return true;
}
getURL() {
return this.url;
}
destroy() {
this.parent?.destroy?.();
}
}
exports.IdentityMapper = IdentityMapper;
/**
* Maps positions in a fragment relative to a parent.
*/
class FragmentMapper {
constructor(originalText, tagInfo, url) {
this.originalText = originalText;
this.tagInfo = tagInfo;
this.url = url;
this.lineOffsetsOriginal = (0, utils_1.getLineOffsets)(this.originalText);
this.lineOffsetsGenerated = (0, utils_1.getLineOffsets)(this.tagInfo.content);
}
getOriginalPosition(generatedPosition) {
const parentOffset = this.offsetInParent((0, utils_1.offsetAt)(generatedPosition, this.tagInfo.content, this.lineOffsetsGenerated));
return (0, utils_1.positionAt)(parentOffset, this.originalText, this.lineOffsetsOriginal);
}
offsetInParent(offset) {
return this.tagInfo.start + offset;
}
getGeneratedPosition(originalPosition) {
const fragmentOffset = (0, utils_1.offsetAt)(originalPosition, this.originalText, this.lineOffsetsOriginal) - this.tagInfo.start;
return (0, utils_1.positionAt)(fragmentOffset, this.tagInfo.content, this.lineOffsetsGenerated);
}
isInGenerated(pos) {
const offset = (0, utils_1.offsetAt)(pos, this.originalText, this.lineOffsetsOriginal);
return offset >= this.tagInfo.start && offset <= this.tagInfo.end;
}
getURL() {
return this.url;
}
}
exports.FragmentMapper = FragmentMapper;
class SourceMapDocumentMapper {
constructor(traceMap, sourceUri, parent) {
this.traceMap = traceMap;
this.sourceUri = sourceUri;
this.parent = parent;
}
getOriginalPosition(generatedPosition) {
if (this.parent) {
generatedPosition = this.parent.getOriginalPosition(generatedPosition);
}
if (generatedPosition.line < 0) {
return { line: -1, character: -1 };
}
const mapped = (0, trace_mapping_1.originalPositionFor)(this.traceMap, {
line: generatedPosition.line + 1,
column: generatedPosition.character,
});
if (!mapped) {
return { line: -1, character: -1 };
}
if (mapped.line === 0) {
// eslint-disable-next-line no-console
console.log('Got 0 mapped line from', generatedPosition, 'col was', mapped.column);
}
return {
line: (mapped.line || 0) - 1,
character: mapped.column || 0,
};
}
getGeneratedPosition(originalPosition) {
if (this.parent) {
originalPosition = this.parent.getGeneratedPosition(originalPosition);
}
const mapped = (0, trace_mapping_1.generatedPositionFor)(this.traceMap, {
line: originalPosition.line + 1,
column: originalPosition.character,
source: this.sourceUri,
});
if (!mapped) {
return { line: -1, character: -1 };
}
const result = {
line: (mapped.line || 0) - 1,
character: mapped.column || 0,
};
if (result.line < 0) {
return result;
}
return result;
}
isInGenerated(position) {
if (this.parent && !this.isInGenerated(position)) {
return false;
}
const generated = this.getGeneratedPosition(position);
return generated.line >= 0;
}
getURL() {
return this.sourceUri;
}
}
exports.SourceMapDocumentMapper = SourceMapDocumentMapper;
class ConsumerDocumentMapper extends SourceMapDocumentMapper {
constructor(traceMap, sourceUri, nrPrependesLines) {
super(traceMap, sourceUri);
this.nrPrependesLines = nrPrependesLines;
}
getOriginalPosition(generatedPosition) {
return super.getOriginalPosition(vscode_languageserver_1.Position.create(generatedPosition.line - this.nrPrependesLines, generatedPosition.character));
}
getGeneratedPosition(originalPosition) {
const result = super.getGeneratedPosition(originalPosition);
result.line += this.nrPrependesLines;
return result;
}
isInGenerated() {
// always return true and map outliers case by case
return true;
}
}
exports.ConsumerDocumentMapper = ConsumerDocumentMapper;
function mapRangeToOriginal(fragment, range) {
// DON'T use Range.create here! Positions might not be mapped
// and therefore return negative numbers, which makes Range.create throw.
// These invalid position need to be handled
// on a case-by-case basis in the calling functions.
const originalRange = {
start: fragment.getOriginalPosition(range.start),
end: fragment.getOriginalPosition(range.end),
};
// Range may be mapped one character short - reverse that for "in the same line" cases
if (originalRange.start.line === originalRange.end.line &&
range.start.line === range.end.line &&
originalRange.end.character - originalRange.start.character === range.end.character - range.start.character - 1) {
originalRange.end.character += 1;
}
return originalRange;
}
exports.mapRangeToOriginal = mapRangeToOriginal;
function mapRangeToGenerated(fragment, range) {
return vscode_languageserver_1.Range.create(fragment.getGeneratedPosition(range.start), fragment.getGeneratedPosition(range.end));
}
exports.mapRangeToGenerated = mapRangeToGenerated;
function mapCompletionItemToOriginal(fragment, item) {
if (!item.textEdit) {
return item;
}
return {
...item,
textEdit: mapEditToOriginal(fragment, item.textEdit),
};
}
exports.mapCompletionItemToOriginal = mapCompletionItemToOriginal;
function mapHoverToParent(fragment, hover) {
if (!hover.range) {
return hover;
}
return { ...hover, range: mapRangeToOriginal(fragment, hover.range) };
}
exports.mapHoverToParent = mapHoverToParent;
function mapObjWithRangeToOriginal(fragment, objWithRange) {
return { ...objWithRange, range: mapRangeToOriginal(fragment, objWithRange.range) };
}
exports.mapObjWithRangeToOriginal = mapObjWithRangeToOriginal;
function mapInsertReplaceEditToOriginal(fragment, edit) {
return {
...edit,
insert: mapRangeToOriginal(fragment, edit.insert),
replace: mapRangeToOriginal(fragment, edit.replace),
};
}
exports.mapInsertReplaceEditToOriginal = mapInsertReplaceEditToOriginal;
function mapEditToOriginal(fragment, edit) {
return vscode_languageserver_1.TextEdit.is(edit) ? mapObjWithRangeToOriginal(fragment, edit) : mapInsertReplaceEditToOriginal(fragment, edit);
}
exports.mapEditToOriginal = mapEditToOriginal;
function mapDiagnosticToGenerated(fragment, diagnostic) {
return { ...diagnostic, range: mapRangeToGenerated(fragment, diagnostic.range) };
}
exports.mapDiagnosticToGenerated = mapDiagnosticToGenerated;
function mapColorPresentationToOriginal(fragment, presentation) {
const item = {
...presentation,
};
if (item.textEdit) {
item.textEdit = mapObjWithRangeToOriginal(fragment, item.textEdit);
}
if (item.additionalTextEdits) {
item.additionalTextEdits = item.additionalTextEdits.map((edit) => mapObjWithRangeToOriginal(fragment, edit));
}
return item;
}
exports.mapColorPresentationToOriginal = mapColorPresentationToOriginal;
function mapSymbolInformationToOriginal(fragment, info) {
return { ...info, location: mapObjWithRangeToOriginal(fragment, info.location) };
}
exports.mapSymbolInformationToOriginal = mapSymbolInformationToOriginal;
function mapLocationLinkToOriginal(fragment, def) {
return vscode_languageserver_1.LocationLink.create(def.targetUri, fragment.getURL() === def.targetUri ? mapRangeToOriginal(fragment, def.targetRange) : def.targetRange, fragment.getURL() === def.targetUri
? mapRangeToOriginal(fragment, def.targetSelectionRange)
: def.targetSelectionRange, def.originSelectionRange ? mapRangeToOriginal(fragment, def.originSelectionRange) : undefined);
}
exports.mapLocationLinkToOriginal = mapLocationLinkToOriginal;
function mapTextDocumentEditToOriginal(fragment, edit) {
if (edit.textDocument.uri !== fragment.getURL()) {
return edit;
}
return vscode_languageserver_1.TextDocumentEdit.create(edit.textDocument, edit.edits.map((textEdit) => mapObjWithRangeToOriginal(fragment, textEdit)));
}
exports.mapTextDocumentEditToOriginal = mapTextDocumentEditToOriginal;
function mapCodeActionToOriginal(fragment, codeAction) {
return vscode_languageserver_1.CodeAction.create(codeAction.title, {
documentChanges: codeAction.edit.documentChanges.map((edit) => mapTextDocumentEditToOriginal(fragment, edit)),
}, codeAction.kind);
}
exports.mapCodeActionToOriginal = mapCodeActionToOriginal;
function mapScriptSpanStartToSnapshot(span, scriptTagSnapshot, tsSnapshot) {
const originalPosition = scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(span.start));
return tsSnapshot.offsetAt(tsSnapshot.getGeneratedPosition(originalPosition));
}
exports.mapScriptSpanStartToSnapshot = mapScriptSpanStartToSnapshot;
function mapFoldingRangeToParent(fragment, foldingRange) {
// Despite FoldingRange asking for a start and end line and a start and end character, FoldingRanges
// don't use the Range type, instead asking for 4 number. Not sure why, but it's not convenient
const range = mapRangeToOriginal(fragment, vscode_languageserver_1.Range.create(foldingRange.startLine, foldingRange.startCharacter || 0, foldingRange.endLine, foldingRange.endCharacter || 0));
return vscode_languageserver_1.FoldingRange.create(range.start.line, range.end.line, foldingRange.startCharacter ? range.start.character : undefined, foldingRange.endCharacter ? range.end.character : undefined, foldingRange.kind);
}
exports.mapFoldingRangeToParent = mapFoldingRangeToParent;
function mapSelectionRangeToParent(fragment, selectionRange) {
const { range, parent } = selectionRange;
return vscode_languageserver_1.SelectionRange.create(mapRangeToOriginal(fragment, range), parent && mapSelectionRangeToParent(fragment, parent));
}
exports.mapSelectionRangeToParent = mapSelectionRangeToParent;

View file

@ -0,0 +1,5 @@
export * from './AstroDocument';
export * from './DocumentBase';
export * from './DocumentManager';
export * from './DocumentMapper';
export * from './utils';

View file

@ -0,0 +1,21 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./AstroDocument"), exports);
__exportStar(require("./DocumentBase"), exports);
__exportStar(require("./DocumentManager"), exports);
__exportStar(require("./DocumentMapper"), exports);
__exportStar(require("./utils"), exports);

View file

@ -0,0 +1,15 @@
interface Frontmatter {
state: null | 'open' | 'closed';
startOffset: null | number;
endOffset: null | number;
}
interface Content {
firstNonWhitespaceOffset: null | number;
}
export interface AstroMetadata {
frontmatter: Frontmatter;
content: Content;
}
/** Parses a document to collect metadata about Astro features */
export declare function parseAstro(content: string): AstroMetadata;
export {};

View file

@ -0,0 +1,63 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseAstro = void 0;
const utils_1 = require("./utils");
/** Parses a document to collect metadata about Astro features */
function parseAstro(content) {
const frontmatter = getFrontmatter(content);
return {
frontmatter,
content: getContent(content, frontmatter),
};
}
exports.parseAstro = parseAstro;
/** Get frontmatter metadata */
function getFrontmatter(content) {
/** Quickly check how many `---` blocks are in the document */
function getFrontmatterState() {
const parts = content.trim().split('---').length;
switch (parts) {
case 1:
return null;
case 2:
return 'open';
default:
return 'closed';
}
}
const state = getFrontmatterState();
/** Construct a range containing the document's frontmatter */
function getFrontmatterOffsets() {
const startOffset = content.indexOf('---');
if (startOffset === -1)
return [null, null];
const endOffset = content.slice(startOffset + 3).indexOf('---') + 3;
if (endOffset === -1)
return [startOffset, null];
return [startOffset, endOffset];
}
const [startOffset, endOffset] = getFrontmatterOffsets();
return {
state,
startOffset,
endOffset,
};
}
/** Get content metadata */
function getContent(content, frontmatter) {
switch (frontmatter.state) {
case null: {
const offset = (0, utils_1.getFirstNonWhitespaceIndex)(content);
return { firstNonWhitespaceOffset: offset === -1 ? null : offset };
}
case 'open': {
return { firstNonWhitespaceOffset: null };
}
case 'closed': {
const { endOffset } = frontmatter;
const end = (endOffset ?? 0) + 3;
const offset = (0, utils_1.getFirstNonWhitespaceIndex)(content.slice(end));
return { firstNonWhitespaceOffset: end + offset };
}
}
}

View file

@ -0,0 +1,13 @@
import { HTMLDocument, Position } from 'vscode-html-languageservice';
import type { AstroDocument } from './AstroDocument';
import { AstroMetadata } from './parseAstro';
/**
* Parses text as HTML
*/
export declare function parseHtml(text: string, frontmatter: AstroMetadata): HTMLDocument;
export interface AttributeContext {
name: string;
inValue: boolean;
valueRange?: [number, number];
}
export declare function getAttributeContextAtPosition(document: AstroDocument, position: Position): AttributeContext | null;

View file

@ -0,0 +1,126 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAttributeContextAtPosition = exports.parseHtml = void 0;
const vscode_html_languageservice_1 = require("vscode-html-languageservice");
const utils_1 = require("./utils");
const parser = (0, vscode_html_languageservice_1.getLanguageService)();
/**
* Parses text as HTML
*/
function parseHtml(text, frontmatter) {
const preprocessed = preprocess(text, frontmatter);
// We can safely only set getText because only this is used for parsing
const parsedDoc = parser.parseHTMLDocument({ getText: () => preprocessed });
return parsedDoc;
}
exports.parseHtml = parseHtml;
const createScanner = parser.createScanner;
/**
* scan the text and remove any `>` or `<` that cause the tag to end short,
*/
function preprocess(text, frontmatter) {
let scanner = createScanner(text);
let token = scanner.scan();
let currentStartTagStart = null;
const hasFrontmatter = frontmatter !== undefined;
while (token !== vscode_html_languageservice_1.TokenType.EOS) {
const offset = scanner.getTokenOffset();
if (hasFrontmatter &&
(scanner.getTokenText() === '>' || scanner.getTokenText() === '<') &&
offset < (frontmatter.content.firstNonWhitespaceOffset ?? 0)) {
blankStartOrEndTagLike(offset, vscode_html_languageservice_1.ScannerState.WithinContent);
}
if (token === vscode_html_languageservice_1.TokenType.StartTagOpen) {
currentStartTagStart = offset;
}
if (token === vscode_html_languageservice_1.TokenType.StartTagClose) {
if (shouldBlankStartOrEndTagLike(offset)) {
blankStartOrEndTagLike(offset);
}
else {
currentStartTagStart = null;
}
}
if (token === vscode_html_languageservice_1.TokenType.StartTagSelfClose) {
currentStartTagStart = null;
}
// <Foo checked={a < 1}>
// https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327
if (token === vscode_html_languageservice_1.TokenType.Unknown &&
scanner.getScannerState() === vscode_html_languageservice_1.ScannerState.WithinTag &&
scanner.getTokenText() === '<' &&
shouldBlankStartOrEndTagLike(offset)) {
blankStartOrEndTagLike(offset);
}
// TODO: Handle TypeScript generics inside expressions / Use the compiler to parse HTML instead?
token = scanner.scan();
}
return text;
function shouldBlankStartOrEndTagLike(offset) {
// not null rather than falsy, otherwise it won't work on first tag(0)
return currentStartTagStart !== null && (0, utils_1.isInsideExpression)(text, currentStartTagStart, offset);
}
function blankStartOrEndTagLike(offset, state) {
text = text.substring(0, offset) + ' ' + text.substring(offset + 1);
scanner = createScanner(text, offset, state ?? vscode_html_languageservice_1.ScannerState.WithinTag);
}
}
function getAttributeContextAtPosition(document, position) {
const offset = document.offsetAt(position);
const { html } = document;
const tag = html.findNodeAt(offset);
if (!inStartTag(offset, tag) || !tag.attributes) {
return null;
}
const text = document.getText();
const beforeStartTagEnd = text.substring(0, tag.start) + preprocess(text.substring(tag.start, tag.startTagEnd));
const scanner = createScanner(beforeStartTagEnd, tag.start);
let token = scanner.scan();
let currentAttributeName;
const inTokenRange = () => scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd();
while (token != vscode_html_languageservice_1.TokenType.EOS) {
// adopted from https://github.com/microsoft/vscode-html-languageservice/blob/2f7ae4df298ac2c299a40e9024d118f4a9dc0c68/src/services/htmlCompletion.ts#L402
if (token === vscode_html_languageservice_1.TokenType.AttributeName) {
currentAttributeName = scanner.getTokenText();
if (inTokenRange()) {
return {
name: currentAttributeName,
inValue: false,
};
}
}
else if (token === vscode_html_languageservice_1.TokenType.DelimiterAssign) {
if (scanner.getTokenEnd() === offset && currentAttributeName) {
const nextToken = scanner.scan();
return {
name: currentAttributeName,
inValue: true,
valueRange: [offset, nextToken === vscode_html_languageservice_1.TokenType.AttributeValue ? scanner.getTokenEnd() : offset],
};
}
}
else if (token === vscode_html_languageservice_1.TokenType.AttributeValue) {
if (inTokenRange() && currentAttributeName) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
const char = text[start];
if (char === '"' || char === "'") {
start++;
end--;
}
return {
name: currentAttributeName,
inValue: true,
valueRange: [start, end],
};
}
currentAttributeName = undefined;
}
token = scanner.scan();
}
return null;
}
exports.getAttributeContextAtPosition = getAttributeContextAtPosition;
function inStartTag(offset, node) {
return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd;
}

View file

@ -0,0 +1,63 @@
import type { HTMLDocument, Node } from 'vscode-html-languageservice';
import { Position } from 'vscode-languageserver';
export interface TagInformation {
content: string;
attributes: Record<string, string>;
start: number;
end: number;
startPos: Position;
endPos: Position;
container: {
start: number;
end: number;
};
closed: boolean;
}
export declare function walk(node: Node): Generator<Node, void, unknown>;
export declare function extractStyleTags(source: string, html: HTMLDocument): TagInformation[];
export declare function extractScriptTags(source: string, html: HTMLDocument): TagInformation[];
export declare function getLineAtPosition(position: Position, text: string): string;
/**
* Return if a given offset is inside the start tag of a component
*/
export declare function isInComponentStartTag(html: HTMLDocument, offset: number): boolean;
/**
* Return if a given offset is inside the name of a tag
*/
export declare function isInTagName(html: HTMLDocument, offset: number): boolean;
/**
* Return true if a specific node could be a component.
* This is not a 100% sure test as it'll return false for any component that does not match the standard format for a component
*/
export declare function isPossibleComponent(node: Node): boolean;
/**
* Return if the current position is in a specific tag
*/
export declare function isInTag(position: Position, tagInfo: TagInformation | null): tagInfo is TagInformation;
/**
* Return if a given position is inside a JSX expression
*/
export declare function isInsideExpression(html: string, tagStart: number, position: number): boolean;
/**
* Returns if a given offset is inside of the document frontmatter
*/
export declare function isInsideFrontmatter(text: string, offset: number): boolean;
/**
* Get the line and character based on the offset
* @param offset The index of the position
* @param text The text for which the position should be retrived
* @param lineOffsets number Array with offsets for each line. Computed if not given
*/
export declare function positionAt(offset: number, text: string, lineOffsets?: number[]): Position;
/**
* Get the offset of the line and character position
* @param position Line and character position
* @param text The text for which the offset should be retrived
* @param lineOffsets number Array with offsets for each line. Computed if not given
*/
export declare function offsetAt(position: Position, text: string, lineOffsets?: number[]): number;
export declare function getLineOffsets(text: string): number[];
/**
* Gets index of first-non-whitespace character.
*/
export declare function getFirstNonWhitespaceIndex(str: string): number;

View file

@ -0,0 +1,223 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFirstNonWhitespaceIndex = exports.getLineOffsets = exports.offsetAt = exports.positionAt = exports.isInsideFrontmatter = exports.isInsideExpression = exports.isInTag = exports.isPossibleComponent = exports.isInTagName = exports.isInComponentStartTag = exports.getLineAtPosition = exports.extractScriptTags = exports.extractStyleTags = exports.walk = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const utils_1 = require("../../utils");
function* walk(node) {
for (let child of node.children) {
yield* walk(child);
}
yield node;
}
exports.walk = walk;
/**
* Extracts a tag (style or script) from the given text
* and returns its start, end and the attributes on that tag.
*
* @param source text content to extract tag from
* @param tag the tag to extract
*/
function extractTags(text, tag, html) {
const rootNodes = html.roots;
const matchedNodes = rootNodes.filter((node) => node.tag === tag);
if (tag === 'style' && !matchedNodes.length && rootNodes.length) {
for (let child of walk(rootNodes[0])) {
if (child.tag === 'style') {
matchedNodes.push(child);
}
}
}
if (tag === 'script' && !matchedNodes.length && rootNodes.length) {
for (let child of walk(rootNodes[0])) {
if (child.tag === 'script') {
matchedNodes.push(child);
}
}
}
return matchedNodes.map(transformToTagInfo);
function transformToTagInfo(matchedNode) {
const start = matchedNode.startTagEnd ?? matchedNode.start;
const end = matchedNode.endTagStart ?? matchedNode.end;
const startPos = positionAt(start, text);
const endPos = positionAt(end, text);
const container = {
start: matchedNode.start,
end: matchedNode.end,
};
const content = text.substring(start, end);
return {
content,
attributes: parseAttributes(matchedNode.attributes),
start,
end,
startPos,
endPos,
container,
// vscode-html-languageservice types does not contain this, despite it existing. Annoying
closed: matchedNode.closed,
};
}
}
function extractStyleTags(source, html) {
const styles = extractTags(source, 'style', html);
if (!styles.length) {
return [];
}
return styles;
}
exports.extractStyleTags = extractStyleTags;
function extractScriptTags(source, html) {
const scripts = extractTags(source, 'script', html);
if (!scripts.length) {
return [];
}
return scripts;
}
exports.extractScriptTags = extractScriptTags;
function parseAttributes(rawAttrs) {
const attrs = {};
if (!rawAttrs) {
return attrs;
}
Object.keys(rawAttrs).forEach((attrName) => {
const attrValue = rawAttrs[attrName];
attrs[attrName] = attrValue === null ? attrName : removeOuterQuotes(attrValue);
});
return attrs;
function removeOuterQuotes(attrValue) {
if ((attrValue.startsWith('"') && attrValue.endsWith('"')) ||
(attrValue.startsWith("'") && attrValue.endsWith("'"))) {
return attrValue.slice(1, attrValue.length - 1);
}
return attrValue;
}
}
function getLineAtPosition(position, text) {
return text.substring(offsetAt({ line: position.line, character: 0 }, text), offsetAt({ line: position.line, character: Number.MAX_VALUE }, text));
}
exports.getLineAtPosition = getLineAtPosition;
/**
* Return if a given offset is inside the start tag of a component
*/
function isInComponentStartTag(html, offset) {
const node = html.findNodeAt(offset);
return isPossibleComponent(node) && (!node.startTagEnd || offset < node.startTagEnd);
}
exports.isInComponentStartTag = isInComponentStartTag;
/**
* Return if a given offset is inside the name of a tag
*/
function isInTagName(html, offset) {
const node = html.findNodeAt(offset);
return offset > node.start && offset < node.start + (node.tag?.length ?? 0);
}
exports.isInTagName = isInTagName;
/**
* Return true if a specific node could be a component.
* This is not a 100% sure test as it'll return false for any component that does not match the standard format for a component
*/
function isPossibleComponent(node) {
return !!node.tag?.[0].match(/[A-Z]/) || !!node.tag?.match(/.+[.][A-Z]?/);
}
exports.isPossibleComponent = isPossibleComponent;
/**
* Return if the current position is in a specific tag
*/
function isInTag(position, tagInfo) {
return !!tagInfo && (0, utils_1.isInRange)(vscode_languageserver_1.Range.create(tagInfo.startPos, tagInfo.endPos), position);
}
exports.isInTag = isInTag;
/**
* Return if a given position is inside a JSX expression
*/
function isInsideExpression(html, tagStart, position) {
const charactersInNode = html.substring(tagStart, position);
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
}
exports.isInsideExpression = isInsideExpression;
/**
* Returns if a given offset is inside of the document frontmatter
*/
function isInsideFrontmatter(text, offset) {
let start = text.slice(0, offset).trim().split('---').length;
let end = text.slice(offset).trim().split('---').length;
return start > 1 && start < 3 && end >= 1;
}
exports.isInsideFrontmatter = isInsideFrontmatter;
/**
* Get the line and character based on the offset
* @param offset The index of the position
* @param text The text for which the position should be retrived
* @param lineOffsets number Array with offsets for each line. Computed if not given
*/
function positionAt(offset, text, lineOffsets = getLineOffsets(text)) {
offset = (0, utils_1.clamp)(offset, 0, text.length);
let low = 0;
let high = lineOffsets.length;
if (high === 0) {
return vscode_languageserver_1.Position.create(0, offset);
}
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const lineOffset = lineOffsets[mid];
if (lineOffset === offset) {
return vscode_languageserver_1.Position.create(mid, 0);
}
else if (offset > lineOffset) {
low = mid + 1;
}
else {
high = mid - 1;
}
}
// low is the least x for which the line offset is larger than the current offset
// or array.length if no line offset is larger than the current offset
const line = low - 1;
return vscode_languageserver_1.Position.create(line, offset - lineOffsets[line]);
}
exports.positionAt = positionAt;
/**
* Get the offset of the line and character position
* @param position Line and character position
* @param text The text for which the offset should be retrived
* @param lineOffsets number Array with offsets for each line. Computed if not given
*/
function offsetAt(position, text, lineOffsets = getLineOffsets(text)) {
if (position.line >= lineOffsets.length) {
return text.length;
}
else if (position.line < 0) {
return 0;
}
const lineOffset = lineOffsets[position.line];
const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length;
return (0, utils_1.clamp)(nextLineOffset, lineOffset, lineOffset + position.character);
}
exports.offsetAt = offsetAt;
function getLineOffsets(text) {
const lineOffsets = [];
let isLineStart = true;
for (let i = 0; i < text.length; i++) {
if (isLineStart) {
lineOffsets.push(i);
isLineStart = false;
}
const ch = text.charAt(i);
isLineStart = ch === '\r' || ch === '\n';
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
i++;
}
}
if (isLineStart && text.length > 0) {
lineOffsets.push(text.length);
}
return lineOffsets;
}
exports.getLineOffsets = getLineOffsets;
/**
* Gets index of first-non-whitespace character.
*/
function getFirstNonWhitespaceIndex(str) {
return str.length - str.trimStart().length;
}
exports.getFirstNonWhitespaceIndex = getFirstNonWhitespaceIndex;

View file

@ -0,0 +1,9 @@
import type * as svelte from '@astrojs/svelte/dist/editor.cjs';
import type * as vue from '@astrojs/svelte/dist/editor.cjs';
import type * as prettier from 'prettier';
export declare function setIsTrusted(_isTrusted: boolean): void;
export declare function getPackagePath(packageName: string, fromPath: string[]): string | undefined;
export declare function importSvelteIntegration(fromPath: string): typeof svelte | undefined;
export declare function importVueIntegration(fromPath: string): typeof vue | undefined;
export declare function importPrettier(fromPath: string): typeof prettier;
export declare function getPrettierPluginPath(fromPath: string): string;

View file

@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPrettierPluginPath = exports.importPrettier = exports.importVueIntegration = exports.importSvelteIntegration = exports.getPackagePath = exports.setIsTrusted = void 0;
const path_1 = require("path");
let isTrusted = true;
function setIsTrusted(_isTrusted) {
isTrusted = _isTrusted;
}
exports.setIsTrusted = setIsTrusted;
function getPackagePath(packageName, fromPath) {
const paths = [];
if (isTrusted) {
paths.unshift(...fromPath);
}
try {
return (0, path_1.dirname)(require.resolve(packageName + '/package.json', { paths }));
}
catch (e) {
return undefined;
}
}
exports.getPackagePath = getPackagePath;
function importEditorIntegration(packageName, fromPath) {
const pkgPath = getPackagePath(packageName, [fromPath]);
if (pkgPath) {
try {
const main = (0, path_1.resolve)(pkgPath, 'dist', 'editor.cjs');
return require(main);
}
catch (e) {
console.error(`Couldn't load editor module from ${pkgPath}. Make sure you're using at least version v0.2.1 of the corresponding integration`);
return undefined;
}
}
return undefined;
}
function importSvelteIntegration(fromPath) {
return importEditorIntegration('@astrojs/svelte', fromPath);
}
exports.importSvelteIntegration = importSvelteIntegration;
function importVueIntegration(fromPath) {
return importEditorIntegration('@astrojs/vue', fromPath);
}
exports.importVueIntegration = importVueIntegration;
function importPrettier(fromPath) {
// This shouldn't ever fail, because we bundle Prettier in the extension itself
const prettierPkg = getPackagePath('prettier', [fromPath, __dirname]);
return require(prettierPkg);
}
exports.importPrettier = importPrettier;
function getPrettierPluginPath(fromPath) {
return getPackagePath('prettier-plugin-astro', [fromPath, __dirname]);
}
exports.getPrettierPluginPath = getPrettierPluginPath;

View file

@ -0,0 +1,2 @@
export { AstroCheck, Diagnostic, DiagnosticSeverity, GetDiagnosticsResult } from './check';
export { offsetAt } from './core/documents';

8
node_modules/@astrojs/language-server/dist/index.js generated vendored Normal file
View file

@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.offsetAt = exports.DiagnosticSeverity = exports.AstroCheck = void 0;
var check_1 = require("./check");
Object.defineProperty(exports, "AstroCheck", { enumerable: true, get: function () { return check_1.AstroCheck; } });
Object.defineProperty(exports, "DiagnosticSeverity", { enumerable: true, get: function () { return check_1.DiagnosticSeverity; } });
var documents_1 = require("./core/documents");
Object.defineProperty(exports, "offsetAt", { enumerable: true, get: function () { return documents_1.offsetAt; } });

1
node_modules/@astrojs/language-server/dist/node.d.ts generated vendored Normal file
View file

@ -0,0 +1 @@
export {};

43
node_modules/@astrojs/language-server/dist/node.js generated vendored Normal file
View file

@ -0,0 +1,43 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const vscode = __importStar(require("vscode-languageserver/node"));
const server_1 = require("./server");
const connection = vscode.createConnection(vscode.ProposedFeatures.all);
(0, server_1.startLanguageServer)(connection, {
loadTypescript(options) {
if (options?.typescript?.serverPath) {
return require(options?.typescript?.serverPath);
}
},
loadTypescriptLocalized(options) {
if (options?.typescript?.localizedPath) {
try {
return require(options?.typescript?.localizedPath);
}
catch { }
}
},
});

View file

@ -0,0 +1,42 @@
import { CancellationToken, CodeAction, CodeActionContext, Color, ColorInformation, ColorPresentation, CompletionContext, CompletionItem, CompletionList, DefinitionLink, Diagnostic, FoldingRange, FormattingOptions, Hover, InlayHint, LinkedEditingRanges, Location, Position, Range, ReferenceContext, SemanticTokens, SignatureHelp, SignatureHelpContext, SymbolInformation, TextDocumentContentChangeEvent, TextDocumentIdentifier, TextEdit, WorkspaceEdit } from 'vscode-languageserver';
import type { DocumentManager } from '../core/documents/DocumentManager';
import type { AppCompletionItem, Plugin } from './interfaces';
export interface PluginHostConfig {
filterIncompleteCompletions: boolean;
definitionLinkSupport: boolean;
}
export declare class PluginHost {
private docManager;
private plugins;
private pluginHostConfig;
constructor(docManager: DocumentManager);
initialize(pluginHostConfig: PluginHostConfig): void;
registerPlugin(plugin: Plugin): void;
getCompletions(textDocument: TextDocumentIdentifier, position: Position, completionContext?: CompletionContext, cancellationToken?: CancellationToken): Promise<CompletionList>;
resolveCompletion(textDocument: TextDocumentIdentifier, completionItem: AppCompletionItem): Promise<CompletionItem>;
getDiagnostics(textDocument: TextDocumentIdentifier): Promise<Diagnostic[]>;
doHover(textDocument: TextDocumentIdentifier, position: Position): Promise<Hover | null>;
formatDocument(textDocument: TextDocumentIdentifier, options: FormattingOptions): Promise<TextEdit[]>;
getCodeActions(textDocument: TextDocumentIdentifier, range: Range, context: CodeActionContext, cancellationToken?: CancellationToken): Promise<CodeAction[]>;
doTagComplete(textDocument: TextDocumentIdentifier, position: Position): Promise<string | null>;
getFoldingRanges(textDocument: TextDocumentIdentifier): Promise<FoldingRange[] | null>;
getDocumentSymbols(textDocument: TextDocumentIdentifier, cancellationToken?: CancellationToken): Promise<SymbolInformation[]>;
getSemanticTokens(textDocument: TextDocumentIdentifier, range?: Range, cancellationToken?: CancellationToken): Promise<SemanticTokens | null>;
getLinkedEditingRanges(textDocument: TextDocumentIdentifier, position: Position): Promise<LinkedEditingRanges | null>;
fileReferences(textDocument: TextDocumentIdentifier): Promise<Location[] | null>;
getDefinitions(textDocument: TextDocumentIdentifier, position: Position): Promise<DefinitionLink[] | Location[]>;
getTypeDefinitions(textDocument: TextDocumentIdentifier, position: Position): Promise<Location[] | null>;
getImplementations(textDocument: TextDocumentIdentifier, position: Position): Promise<Location[] | null>;
getReferences(textdocument: TextDocumentIdentifier, position: Position, context: ReferenceContext): Promise<Location[] | null>;
prepareRename(textDocument: TextDocumentIdentifier, position: Position): Promise<Range | null>;
rename(textDocument: TextDocumentIdentifier, position: Position, newName: string): Promise<WorkspaceEdit | null>;
getDocumentColors(textDocument: TextDocumentIdentifier): Promise<ColorInformation[]>;
getColorPresentations(textDocument: TextDocumentIdentifier, range: Range, color: Color): Promise<ColorPresentation[]>;
getInlayHints(textDocument: TextDocumentIdentifier, range: Range, cancellationToken?: CancellationToken): Promise<InlayHint[]>;
getSignatureHelp(textDocument: TextDocumentIdentifier, position: Position, context: SignatureHelpContext | undefined, cancellationToken?: CancellationToken): Promise<SignatureHelp | null>;
onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void>;
updateNonAstroFile(fileName: string, changes: TextDocumentContentChangeEvent[], text?: string): void;
private getDocument;
private execute;
private tryExecutePlugin;
}

View file

@ -0,0 +1,206 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginHost = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../core/documents");
const utils_1 = require("../utils");
var ExecuteMode;
(function (ExecuteMode) {
ExecuteMode[ExecuteMode["None"] = 0] = "None";
ExecuteMode[ExecuteMode["FirstNonNull"] = 1] = "FirstNonNull";
ExecuteMode[ExecuteMode["Collect"] = 2] = "Collect";
})(ExecuteMode || (ExecuteMode = {}));
class PluginHost {
constructor(docManager) {
this.docManager = docManager;
this.plugins = [];
this.pluginHostConfig = {
filterIncompleteCompletions: true,
definitionLinkSupport: false,
};
}
initialize(pluginHostConfig) {
this.pluginHostConfig = pluginHostConfig;
}
registerPlugin(plugin) {
this.plugins.push(plugin);
}
async getCompletions(textDocument, position, completionContext, cancellationToken) {
const document = this.getDocument(textDocument.uri);
const completions = await Promise.all(this.plugins.map(async (plugin) => {
const result = await this.tryExecutePlugin(plugin, 'getCompletions', [document, position, completionContext, cancellationToken], null);
if (result) {
return { result: result, plugin: plugin.__name };
}
})).then((fullCompletions) => fullCompletions.filter(utils_1.isNotNullOrUndefined));
const html = completions.find((completion) => completion.plugin === 'html');
const ts = completions.find((completion) => completion.plugin === 'typescript');
const astro = completions.find((completion) => completion.plugin === 'astro');
if (html && ts) {
const inComponentStartTag = (0, documents_1.isInComponentStartTag)(document.html, document.offsetAt(position));
// If the HTML plugin returned completions, it's highly likely that TS ones are duplicate
if (html.result.items.length > 0) {
ts.result.items = [];
}
// Inside components, if the Astro plugin has completions we don't want the TS ones are they're duplicates
if (astro && astro.result.items.length > 0 && inComponentStartTag) {
ts.result.items = [];
}
}
let flattenedCompletions = completions.flatMap((completion) => completion.result.items);
const isIncomplete = completions.reduce((incomplete, completion) => incomplete || completion.result.isIncomplete, false);
// If the result is incomplete, we need to filter the results ourselves
// to throw out non-matching results. VSCode does filter client-side,
// but other IDEs might not.
if (isIncomplete && this.pluginHostConfig.filterIncompleteCompletions) {
const offset = document.offsetAt(position);
// Assumption for performance reasons:
// Noone types import names longer than 20 characters and still expects perfect autocompletion.
const text = document.getText().substring(Math.max(0, offset - 20), offset);
const start = (0, utils_1.regexLastIndexOf)(text, /[\W\s]/g) + 1;
const filterValue = text.substring(start).toLowerCase();
flattenedCompletions = flattenedCompletions.filter((comp) => comp.label.toLowerCase().includes(filterValue));
}
return vscode_languageserver_1.CompletionList.create(flattenedCompletions, isIncomplete);
}
async resolveCompletion(textDocument, completionItem) {
const document = this.getDocument(textDocument.uri);
const result = await this.execute('resolveCompletion', [document, completionItem], ExecuteMode.FirstNonNull);
return result ?? completionItem;
}
async getDiagnostics(textDocument) {
const document = this.getDocument(textDocument.uri);
const diagnostics = await this.execute('getDiagnostics', [document], ExecuteMode.Collect);
return diagnostics;
}
async doHover(textDocument, position) {
const document = this.getDocument(textDocument.uri);
return this.execute('doHover', [document, position], ExecuteMode.FirstNonNull);
}
async formatDocument(textDocument, options) {
const document = this.getDocument(textDocument.uri);
return await this.execute('formatDocument', [document, options], ExecuteMode.Collect);
}
async getCodeActions(textDocument, range, context, cancellationToken) {
const document = this.getDocument(textDocument.uri);
return await this.execute('getCodeActions', [document, range, context, cancellationToken], ExecuteMode.Collect);
}
async doTagComplete(textDocument, position) {
const document = this.getDocument(textDocument.uri);
return this.execute('doTagComplete', [document, position], ExecuteMode.FirstNonNull);
}
async getFoldingRanges(textDocument) {
const document = this.getDocument(textDocument.uri);
return await this.execute('getFoldingRanges', [document], ExecuteMode.Collect);
}
async getDocumentSymbols(textDocument, cancellationToken) {
const document = this.getDocument(textDocument.uri);
return await this.execute('getDocumentSymbols', [document, cancellationToken], ExecuteMode.Collect);
}
async getSemanticTokens(textDocument, range, cancellationToken) {
const document = this.getDocument(textDocument.uri);
return await this.execute('getSemanticTokens', [document, range, cancellationToken], ExecuteMode.FirstNonNull);
}
async getLinkedEditingRanges(textDocument, position) {
const document = this.getDocument(textDocument.uri);
return await this.execute('getLinkedEditingRanges', [document, position], ExecuteMode.FirstNonNull);
}
async fileReferences(textDocument) {
const document = this.getDocument(textDocument.uri);
return await this.execute('fileReferences', [document], ExecuteMode.FirstNonNull);
}
async getDefinitions(textDocument, position) {
const document = this.getDocument(textDocument.uri);
const definitions = await this.execute('getDefinitions', [document, position], ExecuteMode.Collect);
if (this.pluginHostConfig.definitionLinkSupport) {
return definitions;
}
else {
return definitions.map((def) => ({ range: def.targetSelectionRange, uri: def.targetUri }));
}
}
getTypeDefinitions(textDocument, position) {
const document = this.getDocument(textDocument.uri);
return this.execute('getTypeDefinitions', [document, position], ExecuteMode.FirstNonNull);
}
getImplementations(textDocument, position) {
const document = this.getDocument(textDocument.uri);
return this.execute('getImplementation', [document, position], ExecuteMode.FirstNonNull);
}
getReferences(textdocument, position, context) {
const document = this.getDocument(textdocument.uri);
return this.execute('findReferences', [document, position, context], ExecuteMode.FirstNonNull);
}
async prepareRename(textDocument, position) {
const document = this.getDocument(textDocument.uri);
return await this.execute('prepareRename', [document, position], ExecuteMode.FirstNonNull);
}
async rename(textDocument, position, newName) {
const document = this.getDocument(textDocument.uri);
return this.execute('rename', [document, position, newName], ExecuteMode.FirstNonNull);
}
async getDocumentColors(textDocument) {
const document = this.getDocument(textDocument.uri);
return await this.execute('getDocumentColors', [document], ExecuteMode.Collect);
}
async getColorPresentations(textDocument, range, color) {
const document = this.getDocument(textDocument.uri);
return await this.execute('getColorPresentations', [document, range, color], ExecuteMode.Collect);
}
async getInlayHints(textDocument, range, cancellationToken) {
const document = this.getDocument(textDocument.uri);
return (await this.execute('getInlayHints', [document, range], ExecuteMode.FirstNonNull)) ?? [];
}
async getSignatureHelp(textDocument, position, context, cancellationToken) {
const document = this.getDocument(textDocument.uri);
return await this.execute('getSignatureHelp', [document, position, context, cancellationToken], ExecuteMode.FirstNonNull);
}
async onWatchFileChanges(onWatchFileChangesParams) {
for (const support of this.plugins) {
await support.onWatchFileChanges?.(onWatchFileChangesParams);
}
}
updateNonAstroFile(fileName, changes, text) {
for (const support of this.plugins) {
support.updateNonAstroFile?.(fileName, changes, text);
}
}
getDocument(uri) {
const document = this.docManager.get(uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}
return document;
}
async execute(name, args, mode) {
const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function');
switch (mode) {
case ExecuteMode.FirstNonNull:
for (const plugin of plugins) {
const res = await this.tryExecutePlugin(plugin, name, args, null);
if (res != null) {
return res;
}
}
return null;
case ExecuteMode.Collect:
return (await Promise.all(plugins.map((plugin) => {
let ret = this.tryExecutePlugin(plugin, name, args, []);
return ret;
}))).flat();
case ExecuteMode.None:
await Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null)));
return;
}
}
async tryExecutePlugin(plugin, fnName, args, failValue) {
try {
return await plugin[fnName](...args);
}
catch (e) {
console.error(e);
return failValue;
}
}
}
exports.PluginHost = PluginHost;

View file

@ -0,0 +1,17 @@
import { CompletionContext, Diagnostic, FoldingRange, FormattingOptions, Position, TextEdit } from 'vscode-languageserver';
import type { ConfigManager } from '../../core/config';
import type { AstroDocument } from '../../core/documents';
import type { AppCompletionList, Plugin } from '../interfaces';
import type { LanguageServiceManager } from '../typescript/LanguageServiceManager';
export declare class AstroPlugin implements Plugin {
__name: string;
private configManager;
private readonly languageServiceManager;
private readonly completionProvider;
private readonly diagnosticsProvider;
constructor(configManager: ConfigManager, languageServiceManager: LanguageServiceManager);
getCompletions(document: AstroDocument, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList | null>;
getDiagnostics(document: AstroDocument): Promise<Diagnostic[]>;
formatDocument(document: AstroDocument, options: FormattingOptions): Promise<TextEdit[]>;
getFoldingRanges(document: AstroDocument): FoldingRange[];
}

View file

@ -0,0 +1,90 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AstroPlugin = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const importPackage_1 = require("../../importPackage");
const CompletionsProvider_1 = require("./features/CompletionsProvider");
const DiagnosticsProvider_1 = require("./features/DiagnosticsProvider");
class AstroPlugin {
constructor(configManager, languageServiceManager) {
this.__name = 'astro';
this.configManager = configManager;
this.languageServiceManager = languageServiceManager;
this.completionProvider = new CompletionsProvider_1.CompletionsProviderImpl(this.languageServiceManager);
this.diagnosticsProvider = new DiagnosticsProvider_1.DiagnosticsProviderImpl(this.languageServiceManager);
}
async getCompletions(document, position, completionContext) {
const completions = this.completionProvider.getCompletions(document, position, completionContext);
return completions;
}
async getDiagnostics(document) {
return await this.diagnosticsProvider.getDiagnostics(document);
}
async formatDocument(document, options) {
const filePath = document.getFilePath();
if (!filePath) {
return [];
}
const prettier = (0, importPackage_1.importPrettier)(filePath);
const prettierConfig = (await prettier.resolveConfig(filePath, { editorconfig: true, useCache: false })) ?? {};
const prettierVSConfig = await this.configManager.getPrettierVSConfig(document);
const editorFormatConfig = options !== undefined // We need to check for options existing here because some editors might not have it
? {
tabWidth: options.tabSize,
useTabs: !options.insertSpaces,
}
: {};
// Return a config with the following cascade:
// - Prettier config file should always win if it exists, if it doesn't:
// - Prettier config from the VS Code extension is used, if it doesn't exist:
// - Use the editor's basic configuration settings
const resultConfig = returnObjectIfHasKeys(prettierConfig) || returnObjectIfHasKeys(prettierVSConfig) || editorFormatConfig;
const fileInfo = await prettier.getFileInfo(filePath, { ignorePath: '.prettierignore' });
if (fileInfo.ignored) {
return [];
}
const result = prettier.format(document.getText(), {
...resultConfig,
plugins: [...getAstroPrettierPlugin(), ...(resultConfig.plugins ?? [])],
parser: 'astro',
});
return document.getText() === result
? []
: [vscode_languageserver_1.TextEdit.replace(vscode_languageserver_1.Range.create(document.positionAt(0), document.positionAt(document.getTextLength())), result)];
function getAstroPrettierPlugin() {
const hasPluginLoadedAlready = prettier.getSupportInfo().languages.some((l) => l.name === 'astro');
return hasPluginLoadedAlready ? [] : [(0, importPackage_1.getPrettierPluginPath)(filePath)];
}
}
getFoldingRanges(document) {
const foldingRanges = [];
const { frontmatter } = document.astroMeta;
// Currently editing frontmatter, don't fold
if (frontmatter.state !== 'closed')
return foldingRanges;
// The way folding ranges work is by folding anything between the starting position and the ending one, as such
// the start in this case should be after the frontmatter start (after the starting ---) until the last character
// of the last line of the frontmatter before its ending (before the closing ---)
// ---
// ^ -- start
// console.log("Astro")
// --- ^ -- end
const start = document.positionAt(frontmatter.startOffset + 3);
const end = document.positionAt(frontmatter.endOffset - 1);
return [
{
startLine: start.line,
startCharacter: start.character,
endLine: end.line,
endCharacter: end.character,
kind: vscode_languageserver_1.FoldingRangeKind.Imports,
},
];
}
}
exports.AstroPlugin = AstroPlugin;
function returnObjectIfHasKeys(obj) {
if (Object.keys(obj || {}).length > 0) {
return obj;
}
}

View file

@ -0,0 +1,17 @@
import { CompletionContext, Position } from 'vscode-languageserver';
import type { AstroDocument } from '../../../core/documents';
import type { AppCompletionList, CompletionsProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../../typescript/LanguageServiceManager';
export declare class CompletionsProviderImpl implements CompletionsProvider {
private readonly languageServiceManager;
private readonly ts;
private lastCompletion;
directivesHTMLLang: import("vscode-html-languageservice").LanguageService;
constructor(languageServiceManager: LanguageServiceManager);
getCompletions(document: AstroDocument, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList | null>;
private getComponentScriptCompletion;
private getPropCompletionsAndFilePath;
private getImportedSymbol;
private getPropType;
private getCompletionItemForProperty;
}

View file

@ -0,0 +1,242 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CompletionsProviderImpl = void 0;
const vscode_html_languageservice_1 = require("vscode-html-languageservice");
const vscode_languageserver_1 = require("vscode-languageserver");
const utils_1 = require("../../../core/documents/utils");
const astro_attributes_1 = require("../../html/features/astro-attributes");
const utils_2 = require("../../html/utils");
const utils_3 = require("../../typescript/utils");
class CompletionsProviderImpl {
constructor(languageServiceManager) {
this.lastCompletion = null;
this.directivesHTMLLang = (0, vscode_html_languageservice_1.getLanguageService)({
customDataProviders: [astro_attributes_1.astroDirectives],
useDefaultDataProvider: false,
});
this.languageServiceManager = languageServiceManager;
this.ts = languageServiceManager.docContext.ts;
}
async getCompletions(document, position, completionContext) {
let items = [];
const html = document.html;
const offset = document.offsetAt(position);
const node = html.findNodeAt(offset);
const insideExpression = (0, utils_1.isInsideExpression)(document.getText(), node.start, offset);
if (completionContext?.triggerCharacter === '-' && node.parent === undefined && !insideExpression) {
const frontmatter = this.getComponentScriptCompletion(document, position);
if (frontmatter)
items.push(frontmatter);
}
if ((0, utils_1.isInComponentStartTag)(html, offset) && !insideExpression) {
const { completions: props, componentFilePath } = await this.getPropCompletionsAndFilePath(document, position, completionContext);
if (props.length) {
items.push(...props);
}
const isAstro = componentFilePath?.endsWith('.astro');
if (!isAstro && node.tag !== 'Fragment') {
const directives = (0, utils_2.removeDataAttrCompletion)(this.directivesHTMLLang.doComplete(document, position, html).items);
items.push(...directives);
}
}
return vscode_languageserver_1.CompletionList.create(items, true);
}
getComponentScriptCompletion(document, position) {
const base = {
kind: vscode_languageserver_1.CompletionItemKind.Snippet,
label: '---',
sortText: '\0',
preselect: true,
detail: 'Create component script block',
insertTextFormat: vscode_languageserver_1.InsertTextFormat.Snippet,
commitCharacters: [],
};
const prefix = document.getLineUntilOffset(document.offsetAt(position));
if (document.astroMeta.frontmatter.state === null) {
return {
...base,
insertText: '---\n$0\n---',
textEdit: prefix.match(/^\s*\-+/)
? vscode_languageserver_1.TextEdit.replace({ start: { ...position, character: 0 }, end: position }, '---\n$0\n---')
: undefined,
};
}
if (document.astroMeta.frontmatter.state === 'open') {
let insertText = '---';
// If the current line is a full component script starter/ender, the user expects a full frontmatter
// completion and not just a completion for "---" on the same line (which result in, well, nothing)
if (prefix === '---') {
insertText = '---\n$0\n---';
}
return {
...base,
insertText,
detail: insertText === '---' ? 'Close component script block' : 'Create component script block',
textEdit: prefix.match(/^\s*\-+/)
? vscode_languageserver_1.TextEdit.replace({ start: { ...position, character: 0 }, end: position }, insertText)
: undefined,
};
}
return null;
}
async getPropCompletionsAndFilePath(document, position, completionContext) {
const offset = document.offsetAt(position);
const html = document.html;
const node = html.findNodeAt(offset);
if (!(0, utils_1.isPossibleComponent)(node)) {
return { completions: [], componentFilePath: null };
}
const inAttribute = node.start + node.tag.length < offset;
if (!inAttribute) {
return { completions: [], componentFilePath: null };
}
if (completionContext?.triggerCharacter === '/' || completionContext?.triggerCharacter === '>') {
return { completions: [], componentFilePath: null };
}
// If inside of attribute value, skip.
if (completionContext &&
completionContext.triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter &&
completionContext.triggerCharacter === '"') {
return { completions: [], componentFilePath: null };
}
const componentName = node.tag;
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
// Get the source file
const tsFilePath = tsDoc.filePath;
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(tsFilePath);
const typeChecker = program?.getTypeChecker();
if (!sourceFile || !typeChecker) {
return { completions: [], componentFilePath: null };
}
// Get the import statement
const imp = this.getImportedSymbol(sourceFile, componentName);
const importType = imp && typeChecker.getTypeAtLocation(imp);
if (!importType) {
return { completions: [], componentFilePath: null };
}
const symbol = importType.getSymbol();
if (!symbol) {
return { completions: [], componentFilePath: null };
}
const symbolDeclaration = symbol.declarations;
if (!symbolDeclaration) {
return { completions: [], componentFilePath: null };
}
const filePath = symbolDeclaration[0].getSourceFile().fileName;
const componentSnapshot = await this.languageServiceManager.getSnapshot(filePath);
if (this.lastCompletion) {
if (this.lastCompletion.tag === componentName &&
this.lastCompletion.documentVersion == componentSnapshot.version) {
return { completions: this.lastCompletion.completions, componentFilePath: filePath };
}
}
// Get the component's props type
const componentType = this.getPropType(symbolDeclaration, typeChecker);
if (!componentType) {
return { completions: [], componentFilePath: null };
}
let completionItems = [];
// Add completions for this component's props type properties
const properties = componentType.getProperties().filter((property) => property.name !== 'children') || [];
properties.forEach((property) => {
const type = typeChecker.getTypeOfSymbolAtLocation(property, imp);
let completionItem = this.getCompletionItemForProperty(property, typeChecker, type);
completionItems.push(completionItem);
});
this.lastCompletion = {
tag: componentName,
documentVersion: componentSnapshot.version,
completions: completionItems,
};
return { completions: completionItems, componentFilePath: filePath };
}
getImportedSymbol(sourceFile, identifier) {
for (let list of sourceFile.getChildren()) {
for (let node of list.getChildren()) {
if (this.ts.isImportDeclaration(node)) {
let clauses = node.importClause;
if (!clauses)
continue;
let namedImport = clauses.getChildAt(0);
if (this.ts.isNamedImports(namedImport)) {
for (let imp of namedImport.elements) {
// Iterate the named imports
if (imp.name.getText() === identifier) {
return imp;
}
}
}
else if (this.ts.isIdentifier(namedImport)) {
if (namedImport.getText() === identifier) {
return namedImport;
}
}
}
}
}
return null;
}
getPropType(declarations, typeChecker) {
for (const decl of declarations) {
const fileName = (0, utils_3.toVirtualFilePath)(decl.getSourceFile().fileName);
if (fileName.endsWith('.tsx') || fileName.endsWith('.jsx') || fileName.endsWith('.d.ts')) {
if (!this.ts.isFunctionDeclaration(decl) && !this.ts.isFunctionTypeNode(decl)) {
console.error(`We only support functions declarations at the moment`);
continue;
}
const fn = decl;
if (!fn.parameters.length)
continue;
const param1 = fn.parameters[0];
const propType = typeChecker.getTypeAtLocation(param1);
return propType;
}
}
return null;
}
getCompletionItemForProperty(mem, typeChecker, type) {
const typeString = typeChecker.typeToString(type);
let insertText = mem.name;
switch (typeString) {
case 'string':
insertText = `${mem.name}="$1"`;
break;
case 'boolean':
insertText = mem.name;
break;
default:
insertText = `${mem.name}={$1}`;
break;
}
let item = {
label: mem.name,
detail: typeString,
insertText: insertText,
insertTextFormat: vscode_languageserver_1.InsertTextFormat.Snippet,
commitCharacters: [],
// Ensure that props shows up first as a completion, despite this plugin being ran after the HTML one
sortText: '\0',
};
if (mem.flags & this.ts.SymbolFlags.Optional) {
item.filterText = item.label;
item.label += '?';
// Put optional props at a lower priority
item.sortText = '_';
}
mem.getDocumentationComment(typeChecker);
let description = mem
.getDocumentationComment(typeChecker)
.map((val) => val.text)
.join('\n');
if (description) {
let docs = {
kind: vscode_languageserver_1.MarkupKind.Markdown,
value: description,
};
item.documentation = docs;
}
return item;
}
}
exports.CompletionsProviderImpl = CompletionsProviderImpl;

View file

@ -0,0 +1,10 @@
import { Diagnostic } from 'vscode-languageserver-types';
import { AstroDocument } from '../../../core/documents';
import { DiagnosticsProvider } from '../../interfaces';
import { LanguageServiceManager } from '../../typescript/LanguageServiceManager';
export declare class DiagnosticsProviderImpl implements DiagnosticsProvider {
private languageServiceManager;
constructor(languageServiceManager: LanguageServiceManager);
getDiagnostics(document: AstroDocument): Promise<Diagnostic[]>;
private compilerMessageToDiagnostic;
}

View file

@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticsProviderImpl = void 0;
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
class DiagnosticsProviderImpl {
constructor(languageServiceManager) {
this.languageServiceManager = languageServiceManager;
}
async getDiagnostics(document) {
const { tsDoc } = (await this.languageServiceManager.getLSAndTSDoc(document));
return tsDoc.compilerDiagnostics.map(this.compilerMessageToDiagnostic);
}
compilerMessageToDiagnostic(message) {
return {
message: message.text + '\n\n' + message.hint,
range: vscode_languageserver_types_1.Range.create(message.location.line - 1, message.location.column - 1, message.location.line, message.location.length),
code: message.code,
severity: message.severity,
source: 'astro',
};
}
}
exports.DiagnosticsProviderImpl = DiagnosticsProviderImpl;

View file

@ -0,0 +1,45 @@
import type { Stylesheet, TextDocument } from 'vscode-css-languageservice';
import type { Position } from 'vscode-languageserver';
import { AstroDocument, DocumentMapper, ReadableDocument, TagInformation } from '../../core/documents';
export interface CSSDocumentBase extends DocumentMapper, TextDocument {
languageId: string;
stylesheet: Stylesheet;
}
export declare class CSSDocument extends ReadableDocument implements DocumentMapper {
private parent;
private styleInfo;
readonly version: number;
stylesheet: Stylesheet;
languageId: string;
constructor(parent: AstroDocument, styleInfo: Pick<TagInformation, 'attributes' | 'start' | 'end'>);
/**
* Get the fragment position relative to the parent
* @param pos Position in fragment
*/
getOriginalPosition(pos: Position): Position;
/**
* Get the position relative to the start of the fragment
* @param pos Position in parent
*/
getGeneratedPosition(pos: Position): Position;
/**
* Returns true if the given parent position is inside of this fragment
* @param pos Position in parent
*/
isInGenerated(pos: Position): boolean;
/**
* Get the fragment text from the parent
*/
getText(): string;
/**
* Returns the length of the fragment as calculated from the start and end position
*/
getTextLength(): number;
/**
* Return the parent file path
*/
getFilePath(): string | null;
getURL(): string;
getAttributes(): Record<string, string>;
private get language();
}

View file

@ -0,0 +1,68 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CSSDocument = void 0;
const documents_1 = require("../../core/documents");
const language_service_1 = require("./language-service");
class CSSDocument extends documents_1.ReadableDocument {
constructor(parent, styleInfo) {
super();
this.parent = parent;
this.styleInfo = styleInfo;
this.version = this.parent.version;
this.languageId = this.language;
this.stylesheet = (0, language_service_1.getLanguageService)(this.language).parseStylesheet(this);
}
/**
* Get the fragment position relative to the parent
* @param pos Position in fragment
*/
getOriginalPosition(pos) {
const parentOffset = this.styleInfo.start + this.offsetAt(pos);
return this.parent.positionAt(parentOffset);
}
/**
* Get the position relative to the start of the fragment
* @param pos Position in parent
*/
getGeneratedPosition(pos) {
const fragmentOffset = this.parent.offsetAt(pos) - this.styleInfo.start;
return this.positionAt(fragmentOffset);
}
/**
* Returns true if the given parent position is inside of this fragment
* @param pos Position in parent
*/
isInGenerated(pos) {
const offset = this.parent.offsetAt(pos);
return offset >= this.styleInfo.start && offset <= this.styleInfo.end;
}
/**
* Get the fragment text from the parent
*/
getText() {
return this.parent.getText().slice(this.styleInfo.start, this.styleInfo.end);
}
/**
* Returns the length of the fragment as calculated from the start and end position
*/
getTextLength() {
return this.styleInfo.end - this.styleInfo.start;
}
/**
* Return the parent file path
*/
getFilePath() {
return this.parent.getFilePath();
}
getURL() {
return this.parent.getURL();
}
getAttributes() {
return this.styleInfo.attributes;
}
get language() {
const attrs = this.getAttributes();
return attrs.lang || attrs.type || 'css';
}
}
exports.CSSDocument = CSSDocument;

View file

@ -0,0 +1,39 @@
import { Color, ColorInformation, ColorPresentation, CompletionContext, CompletionList, FoldingRange, Hover, Position, Range, SymbolInformation, WorkspaceEdit } from 'vscode-languageserver';
import type { ConfigManager } from '../../core/config/ConfigManager';
import { AstroDocument } from '../../core/documents';
import type { Plugin } from '../interfaces';
export declare class CSSPlugin implements Plugin {
__name: string;
private configManager;
private cssDocuments;
private triggerCharacters;
constructor(configManager: ConfigManager);
doHover(document: AstroDocument, position: Position): Promise<Hover | null>;
private doHoverInternal;
getCompletions(document: AstroDocument, position: Position, completionContext?: CompletionContext): Promise<CompletionList | null>;
private getCompletionsInternal;
getDocumentColors(document: AstroDocument): Promise<ColorInformation[]>;
getColorPresentations(document: AstroDocument, range: Range, color: Color): Promise<ColorPresentation[]>;
prepareRename(document: AstroDocument, position: Position): Range | null;
rename(document: AstroDocument, position: Position, newName: string): WorkspaceEdit | null;
getFoldingRanges(document: AstroDocument): FoldingRange[] | null;
getDocumentSymbols(document: AstroDocument): Promise<SymbolInformation[]>;
private inStyleAttributeWithoutInterpolation;
/**
* Get the associated CSS Document for a style tag
*/
private getCSSDocumentForStyleTag;
/**
* Get all the CSSDocuments in a document
*/
private getCSSDocumentsForDocument;
/**
* Get all the stylesheets (Stylesheet type) in a document
*/
private getStylesheetsForDocument;
/**
* Get style tag at position for a document
*/
private getStyleTagForPosition;
private featureEnabled;
}

View file

@ -0,0 +1,274 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CSSPlugin = void 0;
const emmet_helper_1 = require("@vscode/emmet-helper");
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../../core/documents");
const parseHtml_1 = require("../../core/documents/parseHtml");
const CSSDocument_1 = require("./CSSDocument");
const getIdClassCompletions_1 = require("./features/getIdClassCompletions");
const language_service_1 = require("./language-service");
const StyleAttributeDocument_1 = require("./StyleAttributeDocument");
class CSSPlugin {
constructor(configManager) {
this.__name = 'css';
this.cssDocuments = new WeakMap();
this.triggerCharacters = new Set(['.', ':', '-', '/']);
this.configManager = configManager;
}
async doHover(document, position) {
if (!(await this.featureEnabled(document, 'hover'))) {
return null;
}
if ((0, documents_1.isInsideFrontmatter)(document.getText(), document.offsetAt(position))) {
return null;
}
const styleTag = this.getStyleTagForPosition(document, position);
// We technically can return results even for open tags, however, a lot of the info returned is not valid
// Since most editors will automatically close the tag before the user start working in them, this shouldn't be a problem
if (styleTag && !styleTag.closed) {
return null;
}
// If we don't have a style tag at this position, we might be in a style property instead, let's check
if (!styleTag) {
const attributeContext = (0, parseHtml_1.getAttributeContextAtPosition)(document, position);
if (!attributeContext) {
return null;
}
if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
const [start, end] = attributeContext.valueRange;
return this.doHoverInternal(new StyleAttributeDocument_1.StyleAttributeDocument(document, start, end), position);
}
return null;
}
const cssDocument = this.getCSSDocumentForStyleTag(styleTag, document);
const cssLang = extractLanguage(cssDocument);
if (!isSupportedByLangService(cssLang)) {
return null;
}
return this.doHoverInternal(cssDocument, position);
}
doHoverInternal(cssDocument, position) {
const hoverInfo = (0, language_service_1.getLanguageService)(extractLanguage(cssDocument)).doHover(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
return hoverInfo ? (0, documents_1.mapHoverToParent)(cssDocument, hoverInfo) : hoverInfo;
}
async getCompletions(document, position, completionContext) {
if (!(await this.featureEnabled(document, 'completions'))) {
return null;
}
if ((0, documents_1.isInsideFrontmatter)(document.getText(), document.offsetAt(position))) {
return null;
}
const triggerCharacter = completionContext?.triggerCharacter;
const triggerKind = completionContext?.triggerKind;
const isCustomTriggerCharacter = triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter;
if (isCustomTriggerCharacter && triggerCharacter && !this.triggerCharacters.has(triggerCharacter)) {
return null;
}
const styleTag = this.getStyleTagForPosition(document, position);
if (styleTag && !styleTag.closed) {
return null;
}
if (!styleTag) {
const attributeContext = (0, parseHtml_1.getAttributeContextAtPosition)(document, position);
if (!attributeContext) {
return null;
}
if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
const [start, end] = attributeContext.valueRange;
return await this.getCompletionsInternal(document, position, new StyleAttributeDocument_1.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) {
const stylesheets = this.getStylesheetsForDocument(document);
return (0, getIdClassCompletions_1.getIdClassCompletion)(stylesheets, attributeContext);
}
return null;
}
const cssDocument = this.getCSSDocumentForStyleTag(styleTag, document);
return await this.getCompletionsInternal(document, position, cssDocument);
}
async getCompletionsInternal(document, position, cssDocument) {
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 (0, emmet_helper_1.doComplete)(document, position, 'sass', emmetConfig) || null;
}
const cssLang = extractLanguage(cssDocument);
const langService = (0, language_service_1.getLanguageService)(cssLang);
let emmetResults = {
isIncomplete: false,
items: [],
};
const extensionConfig = await this.configManager.getConfig('astro', document.uri);
if (extensionConfig?.css?.completions?.emmet ?? true) {
langService.setCompletionParticipants([
{
onCssProperty: (context) => {
if (context?.propertyName) {
emmetResults =
(0, emmet_helper_1.doComplete)(cssDocument, cssDocument.getGeneratedPosition(position), (0, language_service_1.getLanguage)(cssLang), emmetConfig) || emmetResults;
}
},
onCssPropertyValue: (context) => {
if (context?.propertyValue) {
emmetResults =
(0, emmet_helper_1.doComplete)(cssDocument, cssDocument.getGeneratedPosition(position), (0, language_service_1.getLanguage)(cssLang), emmetConfig) || emmetResults;
}
},
},
]);
}
const results = langService.doComplete(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
return vscode_languageserver_1.CompletionList.create([...(results ? results.items : []), ...emmetResults.items].map((completionItem) => (0, documents_1.mapCompletionItemToOriginal)(cssDocument, completionItem)),
// Emmet completions change on every keystroke, so they are never complete
emmetResults.items.length > 0);
}
async getDocumentColors(document) {
if (!(await this.featureEnabled(document, 'documentColors'))) {
return [];
}
const allColorInfo = this.getCSSDocumentsForDocument(document).flatMap((cssDoc) => {
const cssLang = extractLanguage(cssDoc);
const langService = (0, language_service_1.getLanguageService)(cssLang);
if (!isSupportedByLangService(cssLang)) {
return [];
}
return langService
.findDocumentColors(cssDoc, cssDoc.stylesheet)
.map((colorInfo) => (0, documents_1.mapObjWithRangeToOriginal)(cssDoc, colorInfo));
});
return allColorInfo;
}
async getColorPresentations(document, range, color) {
if (!(await this.featureEnabled(document, 'documentColors'))) {
return [];
}
const allColorPres = this.getCSSDocumentsForDocument(document).flatMap((cssDoc) => {
const cssLang = extractLanguage(cssDoc);
const langService = (0, language_service_1.getLanguageService)(cssLang);
if ((!cssDoc.isInGenerated(range.start) && !cssDoc.isInGenerated(range.end)) ||
!isSupportedByLangService(cssLang)) {
return [];
}
return langService
.getColorPresentations(cssDoc, cssDoc.stylesheet, color, (0, documents_1.mapRangeToGenerated)(cssDoc, range))
.map((colorPres) => (0, documents_1.mapColorPresentationToOriginal)(cssDoc, colorPres));
});
return allColorPres;
}
prepareRename(document, position) {
const styleTag = this.getStyleTagForPosition(document, position);
if (!styleTag) {
return null;
}
const cssDocument = this.getCSSDocumentForStyleTag(styleTag, document);
const cssLang = extractLanguage(cssDocument);
const langService = (0, language_service_1.getLanguageService)(cssLang);
const range = langService.prepareRename(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
if (!range) {
return null;
}
return (0, documents_1.mapRangeToOriginal)(cssDocument, range);
}
rename(document, position, newName) {
const styleTag = this.getStyleTagForPosition(document, position);
if (!styleTag) {
return null;
}
const cssDocument = this.getCSSDocumentForStyleTag(styleTag, document);
const cssLang = extractLanguage(cssDocument);
const langService = (0, language_service_1.getLanguageService)(cssLang);
const renames = langService.doRename(cssDocument, cssDocument.getGeneratedPosition(position), newName, cssDocument.stylesheet);
if (renames?.changes?.[document.uri]) {
renames.changes[document.uri] = renames?.changes?.[document.uri].map((edit) => (0, documents_1.mapObjWithRangeToOriginal)(cssDocument, edit));
}
return renames;
}
getFoldingRanges(document) {
const allFoldingRanges = this.getCSSDocumentsForDocument(document).flatMap((cssDoc) => {
const cssLang = extractLanguage(cssDoc);
const langService = (0, language_service_1.getLanguageService)(cssLang);
return langService.getFoldingRanges(cssDoc).map((foldingRange) => (0, documents_1.mapFoldingRangeToParent)(cssDoc, foldingRange));
});
return allFoldingRanges;
}
async getDocumentSymbols(document) {
if (!(await this.featureEnabled(document, 'documentSymbols'))) {
return [];
}
const allDocumentSymbols = this.getCSSDocumentsForDocument(document).flatMap((cssDoc) => {
return (0, language_service_1.getLanguageService)(extractLanguage(cssDoc))
.findDocumentSymbols(cssDoc, cssDoc.stylesheet)
.map((symbol) => (0, documents_1.mapSymbolInformationToOriginal)(cssDoc, symbol));
});
return allDocumentSymbols;
}
inStyleAttributeWithoutInterpolation(attrContext, text) {
return (attrContext.name === 'style' &&
!!attrContext.valueRange &&
!text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{'));
}
/**
* Get the associated CSS Document for a style tag
*/
getCSSDocumentForStyleTag(tag, document) {
let cssDoc = this.cssDocuments.get(tag);
if (!cssDoc || cssDoc.version < document.version) {
cssDoc = new CSSDocument_1.CSSDocument(document, tag);
this.cssDocuments.set(tag, cssDoc);
}
return cssDoc;
}
/**
* Get all the CSSDocuments in a document
*/
getCSSDocumentsForDocument(document) {
return document.styleTags.map((tag) => this.getCSSDocumentForStyleTag(tag, document));
}
/**
* Get all the stylesheets (Stylesheet type) in a document
*/
getStylesheetsForDocument(document) {
return this.getCSSDocumentsForDocument(document).map((cssDoc) => cssDoc.stylesheet);
}
/**
* Get style tag at position for a document
*/
getStyleTagForPosition(document, position) {
return document.styleTags.find((styleTag) => {
return (0, documents_1.isInTag)(position, styleTag);
});
}
async featureEnabled(document, feature) {
return ((await this.configManager.isEnabled(document, 'css')) &&
(await this.configManager.isEnabled(document, 'css', feature)));
}
}
exports.CSSPlugin = CSSPlugin;
/**
* Check is a CSSDocument's language is supported by the CSS language service
*/
function isSupportedByLangService(language) {
switch (language) {
case 'css':
case 'scss':
case 'less':
return true;
default:
return false;
}
}
function isSASS(document) {
switch (extractLanguage(document)) {
case 'sass':
return true;
default:
return false;
}
}
function extractLanguage(document) {
const lang = document.languageId;
return lang.replace(/^text\//, '');
}

View file

@ -0,0 +1,40 @@
import type { Stylesheet } from 'vscode-css-languageservice';
import type { Position } from 'vscode-languageserver';
import { AstroDocument, DocumentMapper, ReadableDocument } from '../../core/documents';
export declare class StyleAttributeDocument extends ReadableDocument implements DocumentMapper {
private readonly parent;
private readonly attrStart;
private readonly attrEnd;
readonly version: number;
stylesheet: Stylesheet;
languageId: string;
constructor(parent: AstroDocument, attrStart: number, attrEnd: number);
/**
* Get the fragment position relative to the parent
* @param pos Position in fragment
*/
getOriginalPosition(pos: Position): Position;
/**
* Get the position relative to the start of the fragment
* @param pos Position in parent
*/
getGeneratedPosition(pos: Position): Position;
/**
* Returns true if the given parent position is inside of this fragment
* @param pos Position in parent
*/
isInGenerated(pos: Position): boolean;
/**
* Get the fragment text from the parent
*/
getText(): string;
/**
* Returns the length of the fragment as calculated from the start and end position
*/
getTextLength(): number;
/**
* Return the parent file path
*/
getFilePath(): string | null;
getURL(): string;
}

View file

@ -0,0 +1,64 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StyleAttributeDocument = void 0;
const documents_1 = require("../../core/documents");
const language_service_1 = require("./language-service");
const PREFIX = '__ {';
const SUFFIX = '}';
class StyleAttributeDocument extends documents_1.ReadableDocument {
constructor(parent, attrStart, attrEnd) {
super();
this.parent = parent;
this.attrStart = attrStart;
this.attrEnd = attrEnd;
this.version = this.parent.version;
this.languageId = 'css';
this.stylesheet = (0, language_service_1.getLanguageService)(this.languageId).parseStylesheet(this);
}
/**
* Get the fragment position relative to the parent
* @param pos Position in fragment
*/
getOriginalPosition(pos) {
const parentOffset = this.attrStart + this.offsetAt(pos) - PREFIX.length;
return this.parent.positionAt(parentOffset);
}
/**
* Get the position relative to the start of the fragment
* @param pos Position in parent
*/
getGeneratedPosition(pos) {
const fragmentOffset = this.parent.offsetAt(pos) - this.attrStart + PREFIX.length;
return this.positionAt(fragmentOffset);
}
/**
* Returns true if the given parent position is inside of this fragment
* @param pos Position in parent
*/
isInGenerated(pos) {
const offset = this.parent.offsetAt(pos);
return offset >= this.attrStart && offset <= this.attrEnd;
}
/**
* Get the fragment text from the parent
*/
getText() {
return PREFIX + this.parent.getText().slice(this.attrStart, this.attrEnd) + SUFFIX;
}
/**
* Returns the length of the fragment as calculated from the start and end position
*/
getTextLength() {
return PREFIX.length + this.attrEnd - this.attrStart + SUFFIX.length;
}
/**
* Return the parent file path
*/
getFilePath() {
return this.parent.getFilePath();
}
getURL() {
return this.parent.getURL();
}
}
exports.StyleAttributeDocument = StyleAttributeDocument;

View file

@ -0,0 +1,2 @@
import type { IPseudoClassData } from 'vscode-css-languageservice';
export declare const pseudoClass: IPseudoClassData[];

View file

@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.pseudoClass = void 0;
exports.pseudoClass = [
{
name: ':global()',
description: 'Apply styles to a selector globally',
references: [
{
name: 'Astro documentation',
url: 'https://docs.astro.build/en/guides/styling/#global-styles-within-style-tag',
},
],
},
];

View file

@ -0,0 +1,19 @@
import type { Stylesheet } from 'vscode-css-languageservice';
import { CompletionItem, CompletionList } from 'vscode-languageserver';
import type { AttributeContext } from '../../../core/documents/parseHtml';
export declare function getIdClassCompletion(stylesheets: Stylesheet[], attributeContext: AttributeContext): CompletionList | null;
/**
* incomplete see
* https://github.com/microsoft/vscode-css-languageservice/blob/master/src/parser/cssNodes.ts#L14
* The enum is not exported. we have to update this whenever it changes
*/
export declare enum NodeType {
ClassSelector = 14,
IdentifierSelector = 15
}
export type CSSNode = {
type: number;
children: CSSNode[] | undefined;
getText(): string;
};
export declare function collectSelectors(stylesheets: CSSNode[], type: number): CompletionItem[];

View file

@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.collectSelectors = exports.NodeType = exports.getIdClassCompletion = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
function getIdClassCompletion(stylesheets, attributeContext) {
const collectingType = getCollectingType(attributeContext);
if (!collectingType) {
return null;
}
const items = collectSelectors(stylesheets, collectingType);
return vscode_languageserver_1.CompletionList.create(items);
}
exports.getIdClassCompletion = getIdClassCompletion;
function getCollectingType(attributeContext) {
if (attributeContext.inValue) {
if (attributeContext.name === 'class') {
return NodeType.ClassSelector;
}
if (attributeContext.name === 'id') {
return NodeType.IdentifierSelector;
}
}
else if (attributeContext.name.startsWith('class:')) {
return NodeType.ClassSelector;
}
}
/**
* incomplete see
* https://github.com/microsoft/vscode-css-languageservice/blob/master/src/parser/cssNodes.ts#L14
* The enum is not exported. we have to update this whenever it changes
*/
var NodeType;
(function (NodeType) {
NodeType[NodeType["ClassSelector"] = 14] = "ClassSelector";
NodeType[NodeType["IdentifierSelector"] = 15] = "IdentifierSelector";
})(NodeType = exports.NodeType || (exports.NodeType = {}));
function collectSelectors(stylesheets, type) {
const result = [];
stylesheets.forEach((stylesheet) => {
walk(stylesheet, (node) => {
if (node.type === type) {
result.push(node);
}
});
});
return result.map((node) => ({
label: node.getText().substring(1),
kind: vscode_languageserver_1.CompletionItemKind.Keyword,
}));
}
exports.collectSelectors = collectSelectors;
function walk(node, callback) {
callback(node);
if (node.children) {
node.children.forEach((childrenNode) => walk(childrenNode, callback));
}
}

View file

@ -0,0 +1,3 @@
import { LanguageService } from 'vscode-css-languageservice';
export declare function getLanguage(kind?: string): "css" | "less" | "scss";
export declare function getLanguageService(kind?: string): LanguageService;

View file

@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLanguageService = exports.getLanguage = void 0;
const vscode_css_languageservice_1 = require("vscode-css-languageservice");
const astro_selectors_1 = require("./features/astro-selectors");
const customDataProvider = {
providePseudoClasses() {
return astro_selectors_1.pseudoClass;
},
provideProperties() {
return [];
},
provideAtDirectives() {
return [];
},
providePseudoElements() {
return [];
},
};
const [css, scss, less] = [vscode_css_languageservice_1.getCSSLanguageService, vscode_css_languageservice_1.getSCSSLanguageService, vscode_css_languageservice_1.getLESSLanguageService].map((getService) => getService({
customDataProviders: [customDataProvider],
}));
const langs = {
css,
scss,
less,
};
function getLanguage(kind) {
switch (kind) {
case 'scss':
case 'text/scss':
return 'scss';
case 'less':
case 'text/less':
return 'less';
case 'css':
case 'text/css':
default:
return 'css';
}
}
exports.getLanguage = getLanguage;
function getLanguageService(kind) {
const lang = getLanguage(kind);
return langs[lang];
}
exports.getLanguageService = getLanguageService;

View file

@ -0,0 +1,33 @@
import { CompletionList, FoldingRange, Hover, LinkedEditingRanges, Position, Range, SymbolInformation, WorkspaceEdit } from 'vscode-languageserver';
import type { ConfigManager } from '../../core/config/ConfigManager';
import type { AstroDocument } from '../../core/documents/AstroDocument';
import type { Plugin } from '../interfaces';
export declare class HTMLPlugin implements Plugin {
__name: string;
private lang;
private attributeOnlyLang;
private componentLang;
private styleScriptTemplate;
private configManager;
constructor(configManager: ConfigManager);
doHover(document: AstroDocument, position: Position): Promise<Hover | null>;
/**
* Get HTML completions
*/
getCompletions(document: AstroDocument, position: Position): Promise<CompletionList | null>;
getFoldingRanges(document: AstroDocument): FoldingRange[] | null;
getLinkedEditingRanges(document: AstroDocument, position: Position): LinkedEditingRanges | null;
doTagComplete(document: AstroDocument, position: Position): Promise<string | null>;
prepareRename(document: AstroDocument, position: Position): Range | null;
rename(document: AstroDocument, position: Position, newName: string): WorkspaceEdit | null;
getDocumentSymbols(document: AstroDocument): Promise<SymbolInformation[]>;
/**
* Get lang completions for style tags (ex: `<style lang="scss">`)
*/
private getLangCompletions;
/**
* Returns true if rename happens at the tag name, not anywhere inbetween.
*/
private isRenameAtTag;
private featureEnabled;
}

View file

@ -0,0 +1,198 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HTMLPlugin = void 0;
const emmet_helper_1 = require("@vscode/emmet-helper");
const vscode_html_languageservice_1 = require("vscode-html-languageservice");
const vscode_languageserver_1 = require("vscode-languageserver");
const utils_1 = require("../../core/documents/utils");
const astro_attributes_1 = require("./features/astro-attributes");
const utils_2 = require("./utils");
class HTMLPlugin {
constructor(configManager) {
this.__name = 'html';
this.lang = (0, vscode_html_languageservice_1.getLanguageService)({
customDataProviders: [astro_attributes_1.astroAttributes, astro_attributes_1.astroElements, astro_attributes_1.classListAttribute],
});
this.attributeOnlyLang = (0, vscode_html_languageservice_1.getLanguageService)({
customDataProviders: [astro_attributes_1.astroAttributes],
useDefaultDataProvider: false,
});
this.componentLang = (0, vscode_html_languageservice_1.getLanguageService)({
customDataProviders: [astro_attributes_1.astroAttributes, astro_attributes_1.astroDirectives],
useDefaultDataProvider: false,
});
this.styleScriptTemplate = new Set(['style']);
this.configManager = configManager;
}
async doHover(document, position) {
if (!(await this.featureEnabled(document, 'hover'))) {
return null;
}
const html = document.html;
if (!html) {
return null;
}
const node = html.findNodeAt(document.offsetAt(position));
if (!node) {
return null;
}
// If the node we're hovering on is a component, instead only provide astro-specific hover info
if ((0, utils_1.isPossibleComponent)(node) && node.tag !== 'Fragment') {
return this.componentLang.doHover(document, position, html);
}
return this.lang.doHover(document, position, html);
}
/**
* Get HTML completions
*/
async getCompletions(document, position) {
if (!(await this.featureEnabled(document, 'completions'))) {
return null;
}
const html = document.html;
const offset = document.offsetAt(position);
if (!html ||
(0, utils_1.isInsideFrontmatter)(document.getText(), offset) ||
(0, utils_1.isInsideExpression)(document.getText(), html.findNodeAt(offset).start, offset)) {
return null;
}
// Get Emmet completions
let emmetResults = {
isIncomplete: true,
items: [],
};
const emmetConfig = await this.configManager.getEmmetConfig(document);
const extensionConfig = (await this.configManager.getConfig('astro', document.uri)) ?? {};
if (extensionConfig?.html?.completions?.emmet ?? true) {
this.lang.setCompletionParticipants([
{
onHtmlContent: () => (emmetResults = (0, emmet_helper_1.doComplete)(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
const inComponentTag = (0, utils_1.isInComponentStartTag)(html, offset);
const inTagName = (0, utils_1.isInTagName)(html, offset);
const results = inComponentTag && !inTagName
? (0, utils_2.removeDataAttrCompletion)(this.attributeOnlyLang.doComplete(document, position, html).items)
: this.lang.doComplete(document, position, html).items.filter(isNoAddedTagWithNoDocumentation);
const langCompletions = inComponentTag ? [] : this.getLangCompletions(results);
return vscode_languageserver_1.CompletionList.create([...results, ...langCompletions, ...emmetResults.items],
// Emmet completions change on every keystroke, so they are never complete
emmetResults.items.length > 0);
// Filter script and style completions with no documentation to prevent duplicates
// due to our added definitions for those tags
function isNoAddedTagWithNoDocumentation(item) {
return !(['script', 'style'].includes(item.label) && item.documentation === undefined);
}
}
getFoldingRanges(document) {
const html = document.html;
if (!html) {
return null;
}
return this.lang.getFoldingRanges(document);
}
getLinkedEditingRanges(document, position) {
const html = document.html;
if (!html) {
return null;
}
const ranges = this.lang.findLinkedEditingRanges(document, position, html);
if (!ranges) {
return null;
}
return { ranges };
}
async doTagComplete(document, position) {
if (!(await this.featureEnabled(document, 'tagComplete'))) {
return null;
}
const html = document.html;
const offset = document.offsetAt(position);
if (!html ||
(0, utils_1.isInsideFrontmatter)(document.getText(), offset) ||
(0, utils_1.isInsideExpression)(document.getText(), html.findNodeAt(offset).start, offset)) {
return null;
}
return this.lang.doTagComplete(document, position, html);
}
prepareRename(document, position) {
const html = document.html;
const offset = document.offsetAt(position);
const node = html.findNodeAt(offset);
if (!node || (0, utils_1.isPossibleComponent)(node) || !node.tag || !this.isRenameAtTag(node, offset)) {
return null;
}
const tagNameStart = node.start + '<'.length;
return vscode_languageserver_1.Range.create(document.positionAt(tagNameStart), document.positionAt(tagNameStart + node.tag.length));
}
rename(document, position, newName) {
const html = document.html;
const offset = document.offsetAt(position);
if (!html || (0, utils_1.isInsideFrontmatter)(document.getText(), offset)) {
return null;
}
const node = html.findNodeAt(offset);
// The TypeScript plugin handles renaming for components
if (!node || (0, utils_1.isPossibleComponent)(node) || !this.isRenameAtTag(node, offset)) {
return null;
}
return this.lang.doRename(document, position, newName, html);
}
async getDocumentSymbols(document) {
if (!(await this.featureEnabled(document, 'documentSymbols'))) {
return [];
}
const html = document.html;
if (!html) {
return [];
}
return this.lang.findDocumentSymbols(document, html);
}
/**
* Get lang completions for style tags (ex: `<style lang="scss">`)
*/
getLangCompletions(completions) {
const styleScriptTemplateCompletions = completions.filter((completion) => completion.kind === vscode_languageserver_1.CompletionItemKind.Property && this.styleScriptTemplate.has(completion.label));
const langCompletions = [];
addLangCompletion('style', ['scss', 'sass', 'less', 'styl', 'stylus']);
return langCompletions;
/** Add language completions */
function addLangCompletion(tag, languages) {
const existingCompletion = styleScriptTemplateCompletions.find((completion) => completion.label === tag);
if (!existingCompletion) {
return;
}
languages.forEach((lang) => langCompletions.push({
...existingCompletion,
label: `${tag} (lang="${lang}")`,
insertText: existingCompletion.insertText && `${existingCompletion.insertText} lang="${lang}"`,
textEdit: existingCompletion.textEdit && vscode_languageserver_1.TextEdit.is(existingCompletion.textEdit)
? {
range: existingCompletion.textEdit.range,
newText: `${existingCompletion.textEdit.newText} lang="${lang}"`,
}
: undefined,
}));
}
}
/**
* Returns true if rename happens at the tag name, not anywhere inbetween.
*/
isRenameAtTag(node, offset) {
if (!node.tag) {
return false;
}
const startTagNameEnd = node.start + `<${node.tag}`.length;
const isAtStartTag = offset > node.start && offset <= startTagNameEnd;
const isAtEndTag = node.endTagStart !== undefined && offset >= node.endTagStart && offset < node.end;
return isAtStartTag || isAtEndTag;
}
async featureEnabled(document, feature) {
return ((await this.configManager.isEnabled(document, 'html')) &&
(await this.configManager.isEnabled(document, 'html', feature)));
}
}
exports.HTMLPlugin = HTMLPlugin;

View file

@ -0,0 +1,4 @@
export declare const classListAttribute: import("vscode-html-languageservice").IHTMLDataProvider;
export declare const astroElements: import("vscode-html-languageservice").IHTMLDataProvider;
export declare const astroAttributes: import("vscode-html-languageservice").IHTMLDataProvider;
export declare const astroDirectives: import("vscode-html-languageservice").IHTMLDataProvider;

View file

@ -0,0 +1,235 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.astroDirectives = exports.astroAttributes = exports.astroElements = exports.classListAttribute = void 0;
const vscode_html_languageservice_1 = require("vscode-html-languageservice");
const defaultProvider = (0, vscode_html_languageservice_1.getDefaultHTMLDataProvider)();
const slotAttr = defaultProvider.provideAttributes('div').find((attr) => attr.name === 'slot');
exports.classListAttribute = (0, vscode_html_languageservice_1.newHTMLDataProvider)('class-list', {
version: 1,
globalAttributes: [
{
name: 'class:list',
description: 'Utility to provide a list of class',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#classlist',
},
],
},
],
});
exports.astroElements = (0, vscode_html_languageservice_1.newHTMLDataProvider)('astro-elements', {
version: 1,
tags: [
{
name: 'slot',
description: 'The slot element is a placeholder for external HTML content, allowing you to inject (or “slot”) child elements from other files into your component template.',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/core-concepts/astro-components/#slots',
},
],
attributes: [
{
name: 'name',
description: 'The name attribute allows you to pass only HTML elements with the corresponding slot name into a slots location.',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/core-concepts/astro-components/#named-slots',
},
],
},
],
},
{
name: 'script',
attributes: [
{
// The VS Code tag definitions does not provide a description for the deprecated `charset` attribute on script tags
// Which mean that since we get no hover info for this, we instead get JSX hover info. So we'll just specify a description ourselves for this specific case
name: 'charset',
description: "**Deprecated**\n\nIt's unnecessary to specify the charset attribute, because documents must use UTF-8, and the script element inherits its character encoding from the document.",
},
{
name: 'define:vars',
description: 'Passes serializable server-side variables into a client-side script element',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#definevars',
},
],
},
{
name: 'hoist',
description: '**Deprecated in Astro >= 0.26.0**\n\nBuilds, optimizes, and bundles your script with the other JavaScript on the page',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/core-concepts/astro-components/#using-hoisted-scripts',
},
],
},
{
name: 'is:inline',
description: 'Leave a script tag inline in the page template. No processing will be done on its content',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#isinline',
},
],
},
],
},
{
name: 'style',
attributes: [
{
name: 'define:vars',
description: 'Passes serializable server-side variables into a client-side style element',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#definevars',
},
],
},
{
name: 'global',
description: '**Deprecated in favor of `is:global` in >= Astro 0.26.0**\n\nOpts-out of automatic CSS scoping, all contents will be available globally',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#isglobal',
},
],
},
{
name: 'is:global',
description: 'Opts-out of automatic CSS scoping, all contents will be available globally',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#isglobal',
},
],
},
{
name: 'is:inline',
description: 'Leave a style tag inline in the page template. No processing will be done on its content',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#isinline',
},
],
},
],
},
],
});
exports.astroAttributes = (0, vscode_html_languageservice_1.newHTMLDataProvider)('astro-attributes', {
version: 1,
globalAttributes: [
{
name: 'set:html',
description: 'Inject unescaped HTML into this tag',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#sethtml',
},
],
},
{
name: 'set:text',
description: 'Inject escaped text into this tag',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#settext',
},
],
},
{
name: 'is:raw',
description: 'Instructs the Astro compiler to treat any children of this element as text',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#israw',
},
],
},
slotAttr,
],
});
exports.astroDirectives = (0, vscode_html_languageservice_1.newHTMLDataProvider)('astro-directives', {
version: 1,
globalAttributes: [
{
name: 'client:load',
description: 'Start importing the component JS at page load. Hydrate the component when import completes.',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#clientload',
},
],
},
{
name: 'client:idle',
description: 'Start importing the component JS as soon as main thread is free (uses requestIdleCallback()). Hydrate the component when import completes.',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#clientidle',
},
],
},
{
name: 'client:visible',
description: 'Start importing the component JS as soon as the element enters the viewport (uses IntersectionObserver). Hydrate the component when import completes. Useful for content lower down on the page.',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#clientvisible',
},
],
},
{
name: 'client:media',
description: 'Start importing the component JS as soon as the browser matches the given media query (uses matchMedia). Hydrate the component when import completes. Useful for sidebar toggles, or other elements that should only display on mobile or desktop devices.',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#clientmedia',
},
],
},
{
name: 'client:only',
description: 'Start importing the component JS at page load and hydrate when the import completes, similar to client:load. The component will be skipped at build time, useful for components that are entirely dependent on client-side APIs. This is best avoided unless absolutely needed, in most cases it is best to render placeholder content on the server and delay any browser API calls until the component hydrates in the browser.',
valueSet: 'v',
references: [
{
name: 'Astro reference',
url: 'https://docs.astro.build/en/reference/directives-reference/#clientonly',
},
],
},
],
});

View file

@ -0,0 +1,6 @@
import type { CompletionItem } from 'vscode-languageserver-types';
/**
* The VS Code HTML language service provides a completion for data attributes that is independent from
* data providers, which mean that you can't disable it, so this function removes them from completions
*/
export declare function removeDataAttrCompletion(items: CompletionItem[]): CompletionItem[];

View file

@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.removeDataAttrCompletion = void 0;
/**
* The VS Code HTML language service provides a completion for data attributes that is independent from
* data providers, which mean that you can't disable it, so this function removes them from completions
*/
function removeDataAttrCompletion(items) {
return items.filter((item) => !item.label.startsWith('data-'));
}
exports.removeDataAttrCompletion = removeDataAttrCompletion;

View file

@ -0,0 +1,6 @@
export * from './astro/AstroPlugin';
export * from './css/CSSPlugin';
export * from './html/HTMLPlugin';
export * from './interfaces';
export * from './PluginHost';
export * from './typescript/TypeScriptPlugin';

View file

@ -0,0 +1,22 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./astro/AstroPlugin"), exports);
__exportStar(require("./css/CSSPlugin"), exports);
__exportStar(require("./html/HTMLPlugin"), exports);
__exportStar(require("./interfaces"), exports);
__exportStar(require("./PluginHost"), exports);
__exportStar(require("./typescript/TypeScriptPlugin"), exports);

View file

@ -0,0 +1,101 @@
import type { CodeAction, CodeActionContext, Color, ColorInformation, ColorPresentation, CompletionContext, CompletionItem, CompletionList, DefinitionLink, Diagnostic, FileChangeType, FoldingRange, FormattingOptions, Hover, InlayHint, LinkedEditingRanges, Location, Position, Range, ReferenceContext, SelectionRange, SemanticTokens, SignatureHelp, SignatureHelpContext, SymbolInformation, TextDocumentContentChangeEvent, TextDocumentIdentifier, TextEdit, WorkspaceEdit } from 'vscode-languageserver';
import type { TextDocument } from 'vscode-languageserver-textdocument';
export type Resolvable<T> = T | Promise<T>;
export interface AppCompletionItem<T extends TextDocumentIdentifier = any> extends CompletionItem {
data?: T;
}
export interface AppCompletionList<T extends TextDocumentIdentifier = any> extends CompletionList {
items: Array<AppCompletionItem<T>>;
}
export interface DiagnosticsProvider {
getDiagnostics(document: TextDocument): Resolvable<Diagnostic[]>;
}
export interface HoverProvider {
doHover(document: TextDocument, position: Position): Resolvable<Hover | null>;
}
export interface FoldingRangesProvider {
getFoldingRanges(document: TextDocument): Resolvable<FoldingRange[] | null>;
}
export interface CompletionsProvider<T extends TextDocumentIdentifier = any> {
getCompletions(document: TextDocument, position: Position, completionContext?: CompletionContext): Resolvable<AppCompletionList<T> | null>;
resolveCompletion?(document: TextDocument, completionItem: AppCompletionItem<T>): Resolvable<AppCompletionItem<T>>;
}
export interface FormattingProvider {
formatDocument(document: TextDocument, options: FormattingOptions): Resolvable<TextEdit[]>;
}
export interface TagCompleteProvider {
doTagComplete(document: TextDocument, position: Position): Resolvable<string | null>;
}
export interface DocumentColorsProvider {
getDocumentColors(document: TextDocument): Resolvable<ColorInformation[]>;
}
export interface ColorPresentationsProvider {
getColorPresentations(document: TextDocument, range: Range, color: Color): Resolvable<ColorPresentation[]>;
}
export interface DocumentSymbolsProvider {
getDocumentSymbols(document: TextDocument): Resolvable<SymbolInformation[]>;
}
export interface FileReferencesProvider {
fileReferences(document: TextDocument): Promise<Location[] | null>;
}
export interface DefinitionsProvider {
getDefinitions(document: TextDocument, position: Position): Resolvable<DefinitionLink[]>;
}
export interface BackwardsCompatibleDefinitionsProvider {
getDefinitions(document: TextDocument, position: Position): Resolvable<DefinitionLink[] | Location[]>;
}
export interface CodeActionsProvider {
getCodeActions(document: TextDocument, range: Range, context: CodeActionContext): Resolvable<CodeAction[]>;
executeCommand?(document: TextDocument, command: string, args?: any[]): Resolvable<WorkspaceEdit | string | null>;
}
export interface FileRename {
oldUri: string;
newUri: string;
}
export interface UpdateImportsProvider {
updateImports(fileRename: FileRename): Resolvable<WorkspaceEdit | null>;
}
export interface InlayHintsProvider {
getInlayHints(document: TextDocument, range: Range): Resolvable<InlayHint[]>;
}
export interface RenameProvider {
rename(document: TextDocument, position: Position, newName: string): Resolvable<WorkspaceEdit | null>;
prepareRename(document: TextDocument, position: Position): Resolvable<Range | null>;
}
export interface FindReferencesProvider {
findReferences(document: TextDocument, position: Position, context: ReferenceContext): Promise<Location[] | null>;
}
export interface SignatureHelpProvider {
getSignatureHelp(document: TextDocument, position: Position, context: SignatureHelpContext | undefined): Resolvable<SignatureHelp | null>;
}
export interface SelectionRangeProvider {
getSelectionRange(document: TextDocument, position: Position): Resolvable<SelectionRange | null>;
}
export interface SemanticTokensProvider {
getSemanticTokens(textDocument: TextDocument, range?: Range): Resolvable<SemanticTokens | null>;
}
export interface LinkedEditingRangesProvider {
getLinkedEditingRanges(document: TextDocument, position: Position): Resolvable<LinkedEditingRanges | null>;
}
export interface ImplementationProvider {
getImplementation(document: TextDocument, position: Position): Resolvable<Location[] | null>;
}
export interface TypeDefinitionsProvider {
getTypeDefinitions(document: TextDocument, position: Position): Resolvable<Location[] | null>;
}
export interface OnWatchFileChangesParam {
fileName: string;
changeType: FileChangeType;
}
export interface OnWatchFileChangesProvider {
onWatchFileChanges(onWatchFileChangesParams: OnWatchFileChangesParam[]): Promise<void>;
}
export interface UpdateNonAstroFile {
updateNonAstroFile(fileName: string, changes: TextDocumentContentChangeEvent[], text?: string): void;
}
type ProviderBase = DiagnosticsProvider & HoverProvider & CompletionsProvider & FileReferencesProvider & DefinitionsProvider & TypeDefinitionsProvider & ImplementationProvider & FormattingProvider & FoldingRangesProvider & TagCompleteProvider & DocumentColorsProvider & ColorPresentationsProvider & DocumentSymbolsProvider & UpdateImportsProvider & CodeActionsProvider & FindReferencesProvider & RenameProvider & SignatureHelpProvider & SemanticTokensProvider & SelectionRangeProvider & OnWatchFileChangesProvider & LinkedEditingRangesProvider & InlayHintsProvider & UpdateNonAstroFile;
export type LSProvider = ProviderBase;
export type Plugin = Partial<ProviderBase> & {
__name: string;
};
export {};

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,47 @@
import type ts from 'typescript';
import type { TextDocumentContentChangeEvent } from 'vscode-languageserver';
import type { ConfigManager } from '../../core/config';
import type { AstroDocument, DocumentManager } from '../../core/documents';
import { LanguageServiceContainer, LanguageServiceDocumentContext } from './language-service';
import type { DocumentSnapshot } from './snapshots/DocumentSnapshot';
import { SnapshotManager } from './snapshots/SnapshotManager';
export declare class LanguageServiceManager {
private readonly docManager;
private readonly workspaceUris;
private readonly configManager;
docContext: LanguageServiceDocumentContext;
private globalSnapshotManager;
constructor(docManager: DocumentManager, workspaceUris: string[], configManager: ConfigManager, ts: typeof import('typescript/lib/tsserverlibrary'), tsLocalized?: Record<string, string> | undefined);
/**
* Create an AstroDocument (only for astro files)
*/
private createDocument;
getSnapshot(document: AstroDocument): Promise<DocumentSnapshot>;
getSnapshot(pathOrDoc: string | AstroDocument): Promise<DocumentSnapshot>;
/**
* Updates snapshot path in all existing ts services and retrieves snapshot
*/
updateSnapshotPath(oldPath: string, newPath: string): Promise<DocumentSnapshot>;
/**
* Deletes snapshot in all existing ts services
*/
deleteSnapshot(filePath: string): Promise<void>;
/**
* Updates project files in all existing ts services
*/
updateProjectFiles(): Promise<void>;
/**
* Updates file in all ts services where it exists
*/
updateExistingNonAstroFile(path: string, changes?: TextDocumentContentChangeEvent[], text?: string): Promise<void>;
getLSAndTSDoc(document: AstroDocument): Promise<{
tsDoc: DocumentSnapshot;
lang: ts.LanguageService;
}>;
getLSForPath(path: string): Promise<ts.LanguageService>;
getTypeScriptLanguageService(filePath: string): Promise<LanguageServiceContainer>;
/**
* @internal Public for tests only
*/
getSnapshotManager(filePath: string): Promise<SnapshotManager>;
}

View file

@ -0,0 +1,97 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LanguageServiceManager = void 0;
const utils_1 = require("../../utils");
const language_service_1 = require("./language-service");
const SnapshotManager_1 = require("./snapshots/SnapshotManager");
class LanguageServiceManager {
constructor(docManager, workspaceUris, configManager, ts, tsLocalized) {
this.docManager = docManager;
this.workspaceUris = workspaceUris;
this.configManager = configManager;
/**
* Create an AstroDocument (only for astro files)
*/
this.createDocument = (fileName, content) => {
const uri = (0, utils_1.pathToUrl)(fileName);
const document = this.docManager.openDocument({
text: content,
uri,
});
this.docManager.lockDocument(uri);
return document;
};
this.globalSnapshotManager = new SnapshotManager_1.GlobalSnapshotManager(ts);
this.docContext = {
createDocument: this.createDocument,
globalSnapshotManager: this.globalSnapshotManager,
configManager: this.configManager,
ts,
tsLocalized: tsLocalized,
};
const handleDocumentChange = (document) => {
this.getSnapshot(document);
};
docManager.on('documentChange', (0, utils_1.debounceSameArg)(handleDocumentChange, (newDoc, prevDoc) => newDoc.uri === prevDoc?.uri, 1000));
docManager.on('documentOpen', handleDocumentChange);
}
async getSnapshot(pathOrDoc) {
const filePath = typeof pathOrDoc === 'string' ? pathOrDoc : pathOrDoc.getFilePath() || '';
const tsService = await this.getTypeScriptLanguageService(filePath);
return tsService.updateSnapshot(pathOrDoc, this.docContext.ts);
}
/**
* Updates snapshot path in all existing ts services and retrieves snapshot
*/
async updateSnapshotPath(oldPath, newPath) {
await this.deleteSnapshot(oldPath);
return this.getSnapshot(newPath);
}
/**
* Deletes snapshot in all existing ts services
*/
async deleteSnapshot(filePath) {
await (0, language_service_1.forAllLanguageServices)((service) => service.deleteSnapshot(filePath));
this.docManager.releaseDocument((0, utils_1.pathToUrl)(filePath));
}
/**
* Updates project files in all existing ts services
*/
async updateProjectFiles() {
await (0, language_service_1.forAllLanguageServices)((service) => service.updateProjectFiles());
}
/**
* Updates file in all ts services where it exists
*/
async updateExistingNonAstroFile(path, changes, text) {
path = (0, utils_1.normalizePath)(path);
// Only update once because all snapshots are shared between
// services. Since we don't have a current version of TS/JS
// files, the operation wouldn't be idempotent.
let didUpdate = false;
await (0, language_service_1.forAllLanguageServices)((service) => {
if (service.hasFile(path) && !didUpdate) {
didUpdate = true;
service.updateNonAstroFile(path, changes, text);
}
});
}
async getLSAndTSDoc(document) {
const lang = await this.getLSForPath(document.getFilePath() || '');
const tsDoc = await this.getSnapshot(document);
return { tsDoc, lang };
}
async getLSForPath(path) {
return (await this.getTypeScriptLanguageService(path)).getService();
}
async getTypeScriptLanguageService(filePath) {
return (0, language_service_1.getLanguageService)(filePath, this.workspaceUris, this.docContext);
}
/**
* @internal Public for tests only
*/
async getSnapshotManager(filePath) {
return (await this.getTypeScriptLanguageService(filePath)).snapshotManager;
}
}
exports.LanguageServiceManager = LanguageServiceManager;

View file

@ -0,0 +1,54 @@
import type { TSXResult } from '@astrojs/compiler/types';
import { CancellationToken, CodeAction, CodeActionContext, CompletionContext, DefinitionLink, Diagnostic, FoldingRange, Hover, InlayHint, Location, Position, Range, ReferenceContext, SemanticTokens, SignatureHelp, SignatureHelpContext, SymbolInformation, TextDocumentContentChangeEvent, WorkspaceEdit } from 'vscode-languageserver';
import type { ConfigManager } from '../../core/config';
import type { AstroDocument } from '../../core/documents';
import type { AppCompletionItem, AppCompletionList, OnWatchFileChangesParam, Plugin } from '../interfaces';
import { CompletionItemData } from './features/CompletionsProvider';
import type { LanguageServiceManager } from './LanguageServiceManager';
export declare class TypeScriptPlugin implements Plugin {
__name: string;
private configManager;
private readonly languageServiceManager;
private readonly codeActionsProvider;
private readonly completionProvider;
private readonly hoverProvider;
private readonly fileReferencesProvider;
private readonly definitionsProvider;
private readonly typeDefinitionsProvider;
private readonly implementationsProvider;
private readonly referencesProvider;
private readonly signatureHelpProvider;
private readonly diagnosticsProvider;
private readonly documentSymbolsProvider;
private readonly inlayHintsProvider;
private readonly semanticTokensProvider;
private readonly foldingRangesProvider;
private readonly renameProvider;
private readonly ts;
constructor(configManager: ConfigManager, languageServiceManager: LanguageServiceManager);
doHover(document: AstroDocument, position: Position): Promise<Hover | null>;
prepareRename(document: AstroDocument, position: Position): Promise<Range | null>;
rename(document: AstroDocument, position: Position, newName: string): Promise<WorkspaceEdit | null>;
getFoldingRanges(document: AstroDocument): Promise<FoldingRange[] | null>;
getSemanticTokens(document: AstroDocument, range?: Range, cancellationToken?: CancellationToken): Promise<SemanticTokens | null>;
getDocumentSymbols(document: AstroDocument): Promise<SymbolInformation[]>;
getCodeActions(document: AstroDocument, range: Range, context: CodeActionContext, cancellationToken?: CancellationToken): Promise<CodeAction[]>;
getCompletions(document: AstroDocument, position: Position, completionContext?: CompletionContext, cancellationToken?: CancellationToken): Promise<AppCompletionList<CompletionItemData> | null>;
resolveCompletion(document: AstroDocument, completionItem: AppCompletionItem<CompletionItemData>, cancellationToken?: CancellationToken): Promise<AppCompletionItem<CompletionItemData>>;
getInlayHints(document: AstroDocument, range: Range): Promise<InlayHint[]>;
fileReferences(document: AstroDocument): Promise<Location[] | null>;
getDefinitions(document: AstroDocument, position: Position): Promise<DefinitionLink[]>;
getTypeDefinitions(document: AstroDocument, position: Position): Promise<Location[] | null>;
getImplementation(document: AstroDocument, position: Position): Promise<Location[] | null>;
findReferences(document: AstroDocument, position: Position, context: ReferenceContext): Promise<Location[] | null>;
getDiagnostics(document: AstroDocument, cancellationToken?: CancellationToken): Promise<Diagnostic[]>;
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesParam[]): Promise<void>;
updateNonAstroFile(fileName: string, changes: TextDocumentContentChangeEvent[], text?: string): Promise<void>;
getSignatureHelp(document: AstroDocument, position: Position, context: SignatureHelpContext | undefined, cancellationToken?: CancellationToken): Promise<SignatureHelp | null>;
getTSXForDocument(document: AstroDocument): TSXResult;
/**
* @internal Public for tests only
*/
getSnapshotManager(fileName: string): Promise<import("./snapshots/SnapshotManager").SnapshotManager>;
private featureEnabled;
}

View file

@ -0,0 +1,152 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TypeScriptPlugin = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const astro2tsx_1 = require("./astro2tsx");
const CodeActionsProvider_1 = require("./features/CodeActionsProvider");
const CompletionsProvider_1 = require("./features/CompletionsProvider");
const DefinitionsProvider_1 = require("./features/DefinitionsProvider");
const DiagnosticsProvider_1 = require("./features/DiagnosticsProvider");
const DocumentSymbolsProvider_1 = require("./features/DocumentSymbolsProvider");
const FileReferencesProvider_1 = require("./features/FileReferencesProvider");
const FoldingRangesProvider_1 = require("./features/FoldingRangesProvider");
const HoverProvider_1 = require("./features/HoverProvider");
const ImplementationsProvider_1 = require("./features/ImplementationsProvider");
const InlayHintsProvider_1 = require("./features/InlayHintsProvider");
const ReferencesProvider_1 = require("./features/ReferencesProvider");
const RenameProvider_1 = require("./features/RenameProvider");
const SemanticTokenProvider_1 = require("./features/SemanticTokenProvider");
const SignatureHelpProvider_1 = require("./features/SignatureHelpProvider");
const TypeDefinitionsProvider_1 = require("./features/TypeDefinitionsProvider");
const utils_1 = require("./utils");
class TypeScriptPlugin {
constructor(configManager, languageServiceManager) {
this.__name = 'typescript';
this.configManager = configManager;
this.languageServiceManager = languageServiceManager;
this.ts = languageServiceManager.docContext.ts;
this.codeActionsProvider = new CodeActionsProvider_1.CodeActionsProviderImpl(this.languageServiceManager, this.configManager);
this.completionProvider = new CompletionsProvider_1.CompletionsProviderImpl(this.languageServiceManager, this.configManager);
this.hoverProvider = new HoverProvider_1.HoverProviderImpl(this.languageServiceManager);
this.fileReferencesProvider = new FileReferencesProvider_1.FileReferencesProviderImpl(this.languageServiceManager);
this.definitionsProvider = new DefinitionsProvider_1.DefinitionsProviderImpl(this.languageServiceManager);
this.typeDefinitionsProvider = new TypeDefinitionsProvider_1.TypeDefinitionsProviderImpl(this.languageServiceManager);
this.implementationsProvider = new ImplementationsProvider_1.ImplementationsProviderImpl(this.languageServiceManager);
this.referencesProvider = new ReferencesProvider_1.FindReferencesProviderImpl(this.languageServiceManager);
this.signatureHelpProvider = new SignatureHelpProvider_1.SignatureHelpProviderImpl(this.languageServiceManager);
this.diagnosticsProvider = new DiagnosticsProvider_1.DiagnosticsProviderImpl(this.languageServiceManager);
this.documentSymbolsProvider = new DocumentSymbolsProvider_1.DocumentSymbolsProviderImpl(this.languageServiceManager);
this.semanticTokensProvider = new SemanticTokenProvider_1.SemanticTokensProviderImpl(this.languageServiceManager);
this.inlayHintsProvider = new InlayHintsProvider_1.InlayHintsProviderImpl(this.languageServiceManager, this.configManager);
this.foldingRangesProvider = new FoldingRangesProvider_1.FoldingRangesProviderImpl(this.languageServiceManager);
this.renameProvider = new RenameProvider_1.RenameProviderImpl(this.languageServiceManager, this.configManager);
}
async doHover(document, position) {
if (!(await this.featureEnabled(document, 'hover'))) {
return null;
}
return this.hoverProvider.doHover(document, position);
}
async prepareRename(document, position) {
return this.renameProvider.prepareRename(document, position);
}
async rename(document, position, newName) {
return this.renameProvider.rename(document, position, newName);
}
async getFoldingRanges(document) {
return this.foldingRangesProvider.getFoldingRanges(document);
}
async getSemanticTokens(document, range, cancellationToken) {
if (!(await this.featureEnabled(document, 'semanticTokens'))) {
return null;
}
return this.semanticTokensProvider.getSemanticTokens(document, range, cancellationToken);
}
async getDocumentSymbols(document) {
if (!(await this.featureEnabled(document, 'documentSymbols'))) {
return [];
}
const symbols = await this.documentSymbolsProvider.getDocumentSymbols(document);
return symbols;
}
async getCodeActions(document, range, context, cancellationToken) {
if (!(await this.featureEnabled(document, 'codeActions'))) {
return [];
}
return this.codeActionsProvider.getCodeActions(document, range, context, cancellationToken);
}
async getCompletions(document, position, completionContext, cancellationToken) {
if (!(await this.featureEnabled(document, 'completions'))) {
return null;
}
const completions = await this.completionProvider.getCompletions(document, position, completionContext, cancellationToken);
return completions;
}
async resolveCompletion(document, completionItem, cancellationToken) {
return this.completionProvider.resolveCompletion(document, completionItem, cancellationToken);
}
async getInlayHints(document, range) {
return this.inlayHintsProvider.getInlayHints(document, range);
}
async fileReferences(document) {
return this.fileReferencesProvider.fileReferences(document);
}
async getDefinitions(document, position) {
return this.definitionsProvider.getDefinitions(document, position);
}
async getTypeDefinitions(document, position) {
return this.typeDefinitionsProvider.getTypeDefinitions(document, position);
}
async getImplementation(document, position) {
return this.implementationsProvider.getImplementation(document, position);
}
async findReferences(document, position, context) {
return this.referencesProvider.findReferences(document, position, context);
}
async getDiagnostics(document, cancellationToken) {
if (!(await this.featureEnabled(document, 'diagnostics'))) {
return [];
}
return this.diagnosticsProvider.getDiagnostics(document, cancellationToken);
}
async onWatchFileChanges(onWatchFileChangesParas) {
let doneUpdateProjectFiles = false;
for (const { fileName, changeType } of onWatchFileChangesParas) {
const scriptKind = (0, utils_1.getScriptKindFromFileName)(fileName, this.ts);
if (scriptKind === this.ts.ScriptKind.Unknown && !(0, utils_1.isFrameworkFilePath)(fileName) && !(0, utils_1.isAstroFilePath)(fileName)) {
continue;
}
if (changeType === vscode_languageserver_1.FileChangeType.Created && !doneUpdateProjectFiles) {
doneUpdateProjectFiles = true;
await this.languageServiceManager.updateProjectFiles();
}
else if (changeType === vscode_languageserver_1.FileChangeType.Deleted) {
await this.languageServiceManager.deleteSnapshot(fileName);
}
else if (!(0, utils_1.isAstroFilePath)(fileName)) {
// Content updates for Astro files are handled through the documentManager and the 'documentChange' event
await this.languageServiceManager.updateExistingNonAstroFile(fileName);
}
}
}
async updateNonAstroFile(fileName, changes, text) {
await this.languageServiceManager.updateExistingNonAstroFile(fileName, changes, text);
}
async getSignatureHelp(document, position, context, cancellationToken) {
return this.signatureHelpProvider.getSignatureHelp(document, position, context, cancellationToken);
}
getTSXForDocument(document) {
return (0, astro2tsx_1.astro2tsx)(document.getText(), document.getURL());
}
/**
* @internal Public for tests only
*/
getSnapshotManager(fileName) {
return this.languageServiceManager.getSnapshotManager(fileName);
}
async featureEnabled(document, feature) {
return ((await this.configManager.isEnabled(document, 'typescript')) &&
(await this.configManager.isEnabled(document, 'typescript', feature)));
}
}
exports.TypeScriptPlugin = TypeScriptPlugin;

View file

@ -0,0 +1,8 @@
import type ts from 'typescript';
import type { DocumentSnapshot } from './snapshots/DocumentSnapshot';
/**
* This should only be accessed by TS Astro module resolution.
*/
export declare function createAstroSys(getSnapshot: (fileName: string) => DocumentSnapshot, ts: typeof import('typescript/lib/tsserverlibrary')): ts.System & {
deleteFromCache: (path: string) => void;
};

View file

@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAstroSys = void 0;
const utils_1 = require("./utils");
/**
* This should only be accessed by TS Astro module resolution.
*/
function createAstroSys(getSnapshot, ts) {
const fileExistsCache = new Map();
const AstroSys = {
...ts.sys,
fileExists(path) {
path = (0, utils_1.ensureRealFilePath)(path);
const exists = fileExistsCache.get(path) ?? ts.sys.fileExists(path);
fileExistsCache.set(path, exists);
return exists;
},
readFile(path) {
const snapshot = getSnapshot(path);
return snapshot.getText(0, snapshot.getLength());
},
readDirectory(path, extensions, exclude, include, depth) {
const extensionsWithAstro = (extensions ?? []).concat(...['.astro', '.svelte', '.vue', '.md', '.mdx', '.html']);
const result = ts.sys.readDirectory(path, extensionsWithAstro, exclude, include, depth);
return result;
},
deleteFile(path) {
fileExistsCache.delete((0, utils_1.ensureRealFilePath)(path));
return ts.sys.deleteFile?.(path);
},
deleteFromCache(path) {
fileExistsCache.delete((0, utils_1.ensureRealFilePath)(path));
},
};
if (ts.sys.realpath) {
const realpath = ts.sys.realpath;
AstroSys.realpath = function (path) {
if ((0, utils_1.isVirtualFilePath)(path)) {
return realpath((0, utils_1.ensureRealFilePath)(path)) + '.tsx';
}
return realpath(path);
};
}
return AstroSys;
}
exports.createAstroSys = createAstroSys;

View file

@ -0,0 +1,2 @@
import type { TSXResult } from '@astrojs/compiler/types';
export declare function astro2tsx(content: string, fileName: string): TSXResult;

View file

@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.astro2tsx = void 0;
const sync_1 = require("@astrojs/compiler/sync");
function astro2tsx(content, fileName) {
try {
const tsx = (0, sync_1.convertToTSX)(content, { filename: fileName });
return tsx;
}
catch (e) {
console.error(`There was an error transforming ${fileName} to TSX. An empty file will be returned instead. Please create an issue: https://github.com/withastro/language-tools/issues\nError: ${e}.`);
return {
code: '',
map: {
file: fileName,
sources: [],
sourcesContent: [],
names: [],
mappings: '',
version: 0,
},
diagnostics: [],
};
}
}
exports.astro2tsx = astro2tsx;

View file

@ -0,0 +1,17 @@
import type { CancellationToken } from 'vscode-languageserver';
import { CodeAction, CodeActionContext, Range } from 'vscode-languageserver-types';
import type { ConfigManager } from '../../../core/config';
import { AstroDocument } from '../../../core/documents';
import type { CodeActionsProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
export declare const sortImportKind: string;
export declare class CodeActionsProviderImpl implements CodeActionsProvider {
private languageServiceManager;
private configManager;
private ts;
constructor(languageServiceManager: LanguageServiceManager, configManager: ConfigManager);
getCodeActions(document: AstroDocument, range: Range, context: CodeActionContext, cancellationToken?: CancellationToken): Promise<CodeAction[]>;
private getComponentQuickFix;
private organizeSortImports;
private fixIndentationOfImports;
}

View file

@ -0,0 +1,212 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeActionsProviderImpl = exports.sortImportKind = void 0;
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const documents_1 = require("../../../core/documents");
const utils_1 = require("../../../utils");
const utils_2 = require("../utils");
const CompletionsProvider_1 = require("./CompletionsProvider");
const utils_3 = require("./utils");
// These are VS Code specific CodeActionKind so they're not in the language server protocol
exports.sortImportKind = `${vscode_languageserver_types_1.CodeActionKind.Source}.sortImports`;
class CodeActionsProviderImpl {
constructor(languageServiceManager, configManager) {
this.languageServiceManager = languageServiceManager;
this.configManager = configManager;
this.ts = languageServiceManager.docContext.ts;
}
async getCodeActions(document, range, context, cancellationToken) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const tsPreferences = await this.configManager.getTSPreferences(document);
const formatOptions = await this.configManager.getTSFormatConfig(document);
let result = [];
if (cancellationToken?.isCancellationRequested) {
return [];
}
if (context.only?.[0] === vscode_languageserver_types_1.CodeActionKind.SourceOrganizeImports) {
return await this.organizeSortImports(document, false, cancellationToken);
}
// The difference between Sort Imports and Organize Imports is that Sort Imports won't do anything destructive.
// For example, it won't remove unused imports whereas Organize Imports will
if (context.only?.[0] === exports.sortImportKind) {
return await this.organizeSortImports(document, true, cancellationToken);
}
if (context.only?.[0] === vscode_languageserver_types_1.CodeActionKind.Source) {
return [
...(await this.organizeSortImports(document, true, cancellationToken)),
...(await this.organizeSortImports(document, false, cancellationToken)),
];
}
if (context.diagnostics.length && (!context.only || context.only.includes(vscode_languageserver_types_1.CodeActionKind.QuickFix))) {
const errorCodes = context.diagnostics
.map((diag) => Number(diag.code))
// We currently cannot support quick fix for unreachable code properly due to the way our TSX output is structured
.filter((code) => code !== 7027);
const html = document.html;
const node = html.findNodeAt(document.offsetAt(range.start));
let codeFixes;
let isInsideScript = false;
if (node.tag === 'script') {
const { snapshot: scriptTagSnapshot, filePath: scriptFilePath } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node);
const start = scriptTagSnapshot.offsetAt(scriptTagSnapshot.getGeneratedPosition(range.start));
const end = scriptTagSnapshot.offsetAt(scriptTagSnapshot.getGeneratedPosition(range.end));
codeFixes = lang.getCodeFixesAtPosition(scriptFilePath, start, end, errorCodes, formatOptions, tsPreferences);
codeFixes = codeFixes.map((fix) => ({
...fix,
changes: mapScriptTagFixToOriginal(fix.changes, scriptTagSnapshot),
}));
isInsideScript = true;
}
else {
const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.start));
const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.end));
codeFixes = errorCodes.includes(2304)
? this.getComponentQuickFix(start, end, lang, tsDoc.filePath, formatOptions, tsPreferences)
: undefined;
codeFixes =
codeFixes ??
lang.getCodeFixesAtPosition(tsDoc.filePath, start, end, errorCodes, formatOptions, tsPreferences);
}
const codeActions = codeFixes.map((fix) => codeFixToCodeAction(fix, context.diagnostics, context.only ? vscode_languageserver_types_1.CodeActionKind.QuickFix : vscode_languageserver_types_1.CodeActionKind.Empty, isInsideScript, this.ts));
result.push(...codeActions);
}
return result;
function codeFixToCodeAction(codeFix, diagnostics, kind, isInsideScript, ts) {
const documentChanges = codeFix.changes.map((change) => {
return vscode_languageserver_types_1.TextDocumentEdit.create(vscode_languageserver_types_1.OptionalVersionedTextDocumentIdentifier.create(document.getURL(), null), change.textChanges.map((edit) => {
let originalRange = (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, edit.span));
// Inside scripts, we don't need to restrain the insertion of code inside a specific zone as it will be
// restricted to the area of the script tag by default
if (!isInsideScript) {
if (codeFix.fixName === 'import') {
return (0, CompletionsProvider_1.codeActionChangeToTextEdit)(document, tsDoc, false, edit, ts);
}
if (codeFix.fixName === 'fixMissingFunctionDeclaration') {
originalRange = (0, utils_2.checkEndOfFileCodeInsert)(originalRange, document);
}
}
else {
// Make sure new imports are not added on the file line of the script tag
if (codeFix.fixName === 'import') {
const existingLine = (0, documents_1.getLineAtPosition)(document.positionAt(edit.span.start), document.getText());
const isNewImport = !existingLine.trim().startsWith('import');
if (!(edit.newText.startsWith('\n') || edit.newText.startsWith('\r\n')) && isNewImport) {
edit.newText = ts.sys.newLine + edit.newText;
}
}
}
return vscode_languageserver_types_1.TextEdit.replace(originalRange, edit.newText);
}));
});
const codeAction = vscode_languageserver_types_1.CodeAction.create(codeFix.description, {
documentChanges,
}, kind);
codeAction.diagnostics = diagnostics;
return codeAction;
}
function mapScriptTagFixToOriginal(changes, scriptTagSnapshot) {
return changes.map((change) => {
change.textChanges.map((edit) => {
edit.span.start = (0, documents_1.mapScriptSpanStartToSnapshot)(edit.span, scriptTagSnapshot, tsDoc);
return edit;
});
return change;
});
}
}
getComponentQuickFix(start, end, lang, filePath, formatOptions, tsPreferences) {
const sourceFile = lang.getProgram()?.getSourceFile(filePath);
if (!sourceFile) {
return;
}
const node = (0, utils_3.findContainingNode)(sourceFile, {
start,
length: end - start,
}, (n) => this.ts.isJsxClosingElement(n) || this.ts.isJsxOpeningLikeElement(n));
if (!node) {
return;
}
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(), tsPreferences, formatOptions);
if (!completion) {
return;
}
const name = tagName.getText();
const suffixedName = name + '__AstroComponent_';
const toFix = (c) => lang.getCompletionEntryDetails(filePath, end, c.name, {}, c.source, {}, c.data)?.codeActions?.map((a) => ({
...a,
description: (0, utils_2.removeAstroComponentSuffix)(a.description),
fixName: 'import',
})) ?? [];
return completion.entries.filter((c) => c.name === name || c.name === suffixedName).flatMap(toFix);
}
async organizeSortImports(document, skipDestructiveCodeActions = false, cancellationToken) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const filePath = tsDoc.filePath;
if (cancellationToken?.isCancellationRequested) {
return [];
}
let changes = [];
if (document.astroMeta.frontmatter.state === 'closed') {
changes.push(...lang.organizeImports({ fileName: filePath, type: 'file', skipDestructiveCodeActions }, {}, {}));
}
document.scriptTags.forEach((scriptTag) => {
const { filePath: scriptFilePath, snapshot: scriptTagSnapshot } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
const edits = lang.organizeImports({ fileName: scriptFilePath, type: 'file', skipDestructiveCodeActions }, {}, {});
edits.forEach((edit) => {
edit.fileName = tsDoc.filePath;
edit.textChanges = edit.textChanges
.map((change) => {
change.span.start = (0, documents_1.mapScriptSpanStartToSnapshot)(change.span, scriptTagSnapshot, tsDoc);
// If the result ending position is unmapped, it usually means the ending position has a newline
// inside the virtual part of the script tag, so let's just make it a character shorter
const range = (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, change.span));
if (range.end.character === 0 && range.end.line === 0) {
change.span.length -= 1;
}
return change;
})
// Since our last line is a (virtual) export, organize imports will try to rewrite it, so let's only take
// changes that actually happens inside the script tag
.filter((change) => {
return (scriptTagSnapshot.isInGenerated(document.positionAt(change.span.start)) &&
!change.newText.includes('export { }'));
});
return edit;
});
changes.push(...edits);
});
const documentChanges = changes.map((change) => {
return vscode_languageserver_types_1.TextDocumentEdit.create(vscode_languageserver_types_1.OptionalVersionedTextDocumentIdentifier.create(document.url, null), change.textChanges.map((edit) => {
const range = (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, edit.span));
if (document.offsetAt(range.start) < (document.astroMeta.content.firstNonWhitespaceOffset ?? 0)) {
if (document.offsetAt(range.end) > document.astroMeta.frontmatter.endOffset) {
range.end = document.positionAt(document.astroMeta.frontmatter.endOffset);
}
}
return vscode_languageserver_types_1.TextEdit.replace(range, this.fixIndentationOfImports(edit.newText, range, document));
}));
});
return [
vscode_languageserver_types_1.CodeAction.create(skipDestructiveCodeActions ? 'Sort Imports' : 'Organize Imports', {
documentChanges,
}, skipDestructiveCodeActions ? exports.sortImportKind : vscode_languageserver_types_1.CodeActionKind.SourceOrganizeImports),
];
}
// "Organize Imports" will have edits that delete all imports by return empty edits
// and one edit which contains all the organized imports. Fix indentation
// of that one by prepending all lines with the indentation of the first line.
fixIndentationOfImports(edit, range, document) {
if (!edit || range.start.character === 0) {
return edit;
}
const existingLine = (0, documents_1.getLineAtPosition)(range.start, document.getText());
const leadingChars = existingLine.substring(0, range.start.character);
if (leadingChars.trim() !== '') {
return edit;
}
return (0, utils_1.modifyLines)(edit, (line, idx) => (idx === 0 || !line ? line : leadingChars + line));
}
}
exports.CodeActionsProviderImpl = CodeActionsProviderImpl;

View file

@ -0,0 +1,31 @@
import type ts from 'typescript';
import { CancellationToken, CompletionContext, Position, TextDocumentIdentifier, TextEdit } from 'vscode-languageserver';
import type { ConfigManager } from '../../../core/config';
import { AstroDocument } from '../../../core/documents';
import type { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
import type { DocumentSnapshot } from '../snapshots/DocumentSnapshot';
export interface CompletionItemData extends TextDocumentIdentifier {
filePath: string;
offset: number;
scriptTagIndex: number | undefined;
originalItem: ts.CompletionEntry;
}
export declare class CompletionsProviderImpl implements CompletionsProvider<CompletionItemData> {
private languageServiceManager;
private configManager;
private ts;
constructor(languageServiceManager: LanguageServiceManager, configManager: ConfigManager);
private readonly validTriggerCharacters;
private isValidTriggerCharacter;
private lastCompletion?;
getCompletions(document: AstroDocument, position: Position, completionContext?: CompletionContext, cancellationToken?: CancellationToken): Promise<AppCompletionList<CompletionItemData> | null>;
resolveCompletion(document: AstroDocument, item: AppCompletionItem<CompletionItemData>, cancellationToken?: CancellationToken): Promise<AppCompletionItem<CompletionItemData>>;
private toCompletionItem;
private getCompletionDocument;
private canReuseLastCompletion;
private getExistingImports;
private isAstroComponentImport;
private isValidCompletion;
}
export declare function codeActionChangeToTextEdit(document: AstroDocument, snapshot: DocumentSnapshot, isInsideScriptTag: boolean, change: ts.TextChange, ts: typeof import('typescript/lib/tsserverlibrary')): TextEdit;

View file

@ -0,0 +1,294 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.codeActionChangeToTextEdit = exports.CompletionsProviderImpl = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
const documents_1 = require("../../../core/documents");
const utils_1 = require("../../../core/documents/utils");
const utils_2 = require("../../../utils");
const previewer_1 = require("../previewer");
const utils_3 = require("../utils");
const utils_4 = require("./utils");
// `import {...} from '..'` or `import ... from '..'`
// Note: Does not take into account if import is within a comment.
const scriptImportRegex = /\bimport\s+{([^}]*?)}\s+?from\s+['"`].+?['"`]|\bimport\s+(\w+?)\s+from\s+['"`].+?['"`]/g;
// When Svelte components are imported, we have to reference the svelte2tsx's types to properly type the component
// An unfortunate downside of this is that it polutes completions, so let's filter those internal types manually
const svelte2tsxTypes = new Set([
'Svelte2TsxComponent',
'Svelte2TsxComponentConstructorParameters',
'SvelteComponentConstructor',
'SvelteActionReturnType',
'SvelteTransitionConfig',
'SvelteTransitionReturnType',
'SvelteAnimationReturnType',
'SvelteWithOptionalProps',
'SvelteAllProps',
'SveltePropsAnyFallback',
'SvelteSlotsAnyFallback',
'SvelteRestProps',
'SvelteSlots',
'SvelteStore',
]);
class CompletionsProviderImpl {
constructor(languageServiceManager, configManager) {
this.languageServiceManager = languageServiceManager;
this.configManager = configManager;
this.validTriggerCharacters = ['.', '"', "'", '`', '/', '@', '<', '#'];
this.ts = languageServiceManager.docContext.ts;
}
isValidTriggerCharacter(character) {
return this.validTriggerCharacters.includes(character);
}
async getCompletions(document, position, completionContext, cancellationToken) {
const triggerCharacter = completionContext?.triggerCharacter;
const triggerKind = completionContext?.triggerKind;
const validTriggerCharacter = this.isValidTriggerCharacter(triggerCharacter) ? triggerCharacter : undefined;
const isCustomTriggerCharacter = triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter;
if ((isCustomTriggerCharacter && !validTriggerCharacter) || cancellationToken?.isCancellationRequested) {
return null;
}
if (this.canReuseLastCompletion(this.lastCompletion, triggerKind, triggerCharacter, document, position)) {
this.lastCompletion.position = position;
return this.lastCompletion.completionList;
}
else {
this.lastCompletion = undefined;
}
const html = document.html;
const documentOffset = document.offsetAt(position);
const node = html.findNodeAt(documentOffset);
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
let filePath = tsDoc.filePath;
let completions;
const isCompletionInsideFrontmatter = (0, utils_1.isInsideFrontmatter)(document.getText(), documentOffset);
const isCompletionInsideExpression = (0, utils_1.isInsideExpression)(document.getText(), node.start, documentOffset);
const tsPreferences = await this.configManager.getTSPreferences(document);
const formatOptions = await this.configManager.getTSFormatConfig(document);
let scriptTagIndex = undefined;
if (node.tag === 'script') {
const { filePath: scriptFilePath, offset: scriptOffset, index: scriptIndex, } = (0, utils_3.getScriptTagSnapshot)(tsDoc, document, node, position);
filePath = scriptFilePath;
scriptTagIndex = scriptIndex;
completions = lang.getCompletionsAtPosition(scriptFilePath, scriptOffset, {
...tsPreferences,
triggerCharacter: validTriggerCharacter,
}, formatOptions);
}
else {
// PERF: Getting TS completions is fairly slow and I am currently not sure how to speed it up
// As such, we'll try to avoid getting them when unneeded, such as when we're doing HTML stuff
// If the user just typed `<` with nothing else, let's disable ourselves until we're more sure if the user wants TS completions
if (!isCompletionInsideFrontmatter && node.parent && node.tag === undefined && !isCompletionInsideExpression) {
return null;
}
// If the current node is not a component, let's disable ourselves as the user
// is most likely looking for HTML completions
if (!isCompletionInsideFrontmatter && !(0, utils_1.isPossibleComponent)(node) && !isCompletionInsideExpression) {
return null;
}
completions = lang.getCompletionsAtPosition(filePath, offset, {
...tsPreferences,
triggerCharacter: validTriggerCharacter,
}, formatOptions);
}
if (completions === undefined || completions.entries.length === 0) {
return null;
}
const existingImports = this.getExistingImports(document);
const completionItems = completions.entries
.filter((completion) => this.isValidCompletion(completion, this.ts))
.map((entry) => this.toCompletionItem(tsDoc, entry, filePath, offset, isCompletionInsideFrontmatter, scriptTagIndex, existingImports))
.filter(utils_2.isNotNullOrUndefined);
const completionList = vscode_languageserver_1.CompletionList.create(completionItems, true);
this.lastCompletion = { key: document.getFilePath() || '', position, completionList };
return completionList;
}
async resolveCompletion(document, item, cancellationToken) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const tsPreferences = await this.configManager.getTSPreferences(document);
const data = item.data;
if (!data || !data.filePath || cancellationToken?.isCancellationRequested) {
return item;
}
const detail = lang.getCompletionEntryDetails(data.filePath, // fileName
data.offset, // position
data.originalItem.name, // entryName
{}, // formatOptions
data.originalItem.source, // source
tsPreferences, // preferences
data.originalItem.data // data
);
if (detail) {
const { detail: itemDetail, documentation: itemDocumentation } = this.getCompletionDocument(detail);
// TODO: Add support for labelDetails
// if (data.originalItem.source) {
// item.labelDetails = { description: data.originalItem.source };
// }
item.detail = itemDetail;
item.documentation = itemDocumentation;
}
const actions = detail?.codeActions;
const isInsideScriptTag = data.scriptTagIndex !== undefined;
let scriptTagSnapshot;
if (isInsideScriptTag) {
const { snapshot } = (0, utils_3.getScriptTagSnapshot)(tsDoc, document, document.scriptTags[data.scriptTagIndex].container);
scriptTagSnapshot = snapshot;
}
if (actions) {
const edit = [];
for (const action of actions) {
for (const change of action.changes) {
if (isInsideScriptTag) {
change.textChanges.forEach((textChange) => {
const originalPosition = scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(textChange.span.start));
textChange.span.start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(originalPosition));
});
}
edit.push(...change.textChanges.map((textChange) => codeActionChangeToTextEdit(document, tsDoc, isInsideScriptTag, textChange, this.ts)));
}
}
item.additionalTextEdits = (item.additionalTextEdits ?? []).concat(edit);
}
return item;
}
toCompletionItem(snapshot, comp, filePath, offset, insideFrontmatter, scriptTagIndex, existingImports) {
let item = vscode_languageserver_protocol_1.CompletionItem.create(comp.name);
const isAstroComponent = this.isAstroComponentImport(comp.name);
const isImport = comp.insertText?.includes('import');
// Avoid showing completions for using components as functions
if (isAstroComponent && !isImport && insideFrontmatter) {
return null;
}
if (isAstroComponent) {
item.label = (0, utils_3.removeAstroComponentSuffix)(comp.name);
// Set component imports as file completion, that way we get cool icons
item.kind = vscode_languageserver_protocol_1.CompletionItemKind.File;
item.detail = comp.data?.moduleSpecifier;
}
else {
item.kind = (0, utils_3.scriptElementKindToCompletionItemKind)(comp.kind, this.ts);
}
// TS may suggest another component even if there already exists an import with the same.
// This happens because internally, components get suffixed with __AstroComponent_
if (isAstroComponent && existingImports.has(item.label)) {
return null;
}
if (comp.kindModifiers) {
const kindModifiers = new Set(comp.kindModifiers.split(/,|\s+/g));
if (kindModifiers.has(this.ts.ScriptElementKindModifier.optionalModifier)) {
if (!item.insertText) {
item.insertText = item.label;
}
if (!item.filterText) {
item.filterText = item.label;
}
item.label += '?';
}
if (kindModifiers.has(this.ts.ScriptElementKindModifier.deprecatedModifier)) {
item.tags = [vscode_languageserver_1.CompletionItemTag.Deprecated];
}
}
// TODO: Add support for labelDetails
// if (comp.sourceDisplay) {
// item.labelDetails = { description: ts.displayPartsToString(comp.sourceDisplay) };
// }
item.commitCharacters = (0, utils_3.getCommitCharactersForScriptElement)(comp.kind, this.ts);
item.sortText = comp.sortText;
item.preselect = comp.isRecommended;
if (comp.replacementSpan) {
item.insertText = comp.insertText ? (0, utils_3.removeAstroComponentSuffix)(comp.insertText) : undefined;
item.insertTextFormat = comp.isSnippet ? vscode_languageserver_1.InsertTextFormat.Snippet : vscode_languageserver_1.InsertTextFormat.PlainText;
item.textEdit = comp.replacementSpan
? vscode_languageserver_1.TextEdit.replace((0, utils_3.convertToLocationRange)(snapshot, comp.replacementSpan), item.insertText ?? item.label)
: undefined;
}
return {
...item,
data: {
uri: snapshot.getURL(),
filePath,
scriptTagIndex,
offset,
originalItem: comp,
},
};
}
getCompletionDocument(compDetail) {
const { sourceDisplay, documentation: tsDocumentation, displayParts } = compDetail;
let detail = (0, utils_3.removeAstroComponentSuffix)(this.ts.displayPartsToString(displayParts));
if (sourceDisplay) {
const importPath = this.ts.displayPartsToString(sourceDisplay);
detail = importPath;
}
const documentation = {
kind: 'markdown',
value: (0, previewer_1.getMarkdownDocumentation)(tsDocumentation, compDetail.tags, this.ts),
};
return {
documentation,
detail,
};
}
canReuseLastCompletion(lastCompletion, triggerKind, triggerCharacter, document, position) {
return (!!lastCompletion &&
lastCompletion.key === document.getFilePath() &&
lastCompletion.position.line === position.line &&
Math.abs(lastCompletion.position.character - position.character) < 2 &&
(triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerForIncompleteCompletions ||
// Special case: `.` is a trigger character, but inside import path completions
// it shouldn't trigger another completion because we can reuse the old one
(triggerCharacter === '.' && (0, utils_4.isPartOfImportStatement)(document.getText(), position))));
}
getExistingImports(document) {
const rawImports = (0, utils_2.getRegExpMatches)(scriptImportRegex, document.getText()).map((match) => (match[1] ?? match[2]).split(','));
const tidiedImports = rawImports.flat().map((match) => match.trim());
return new Set(tidiedImports);
}
isAstroComponentImport(className) {
return className.endsWith('__AstroComponent_');
}
isValidCompletion(completion, ts) {
// Remove completion for default exported function
const isDefaultExport = completion.name === 'default' && completion.kindModifiers == ts.ScriptElementKindModifier.exportedModifier;
// Remove completion for svelte2tsx internal types
const isSvelte2tsxCompletion = completion.name.startsWith('__sveltets_') || svelte2tsxTypes.has(completion.name);
if (isDefaultExport || isSvelte2tsxCompletion) {
return false;
}
return true;
}
}
exports.CompletionsProviderImpl = CompletionsProviderImpl;
function codeActionChangeToTextEdit(document, snapshot, isInsideScriptTag, change, ts) {
change.newText = (0, utils_3.removeAstroComponentSuffix)(change.newText);
const { span } = change;
let range;
const virtualRange = (0, utils_3.convertRange)(snapshot, span);
range = (0, documents_1.mapRangeToOriginal)(snapshot, virtualRange);
if (!isInsideScriptTag) {
// If we don't have a frontmatter already, create one with the import
const frontmatterState = document.astroMeta.frontmatter.state;
if (frontmatterState === null) {
return vscode_languageserver_1.TextEdit.replace(vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(0, 0), vscode_languageserver_1.Position.create(0, 0)), `---${ts.sys.newLine}${change.newText}---${ts.sys.newLine}${ts.sys.newLine}`);
}
if (!(0, utils_1.isInsideFrontmatter)(document.getText(), document.offsetAt(range.start))) {
range = (0, utils_3.ensureFrontmatterInsert)(range, document);
}
// First import in a file will wrongly have a newline before it due to how the frontmatter is replaced by a comment
if (range.start.line === 1 && (change.newText.startsWith('\n') || change.newText.startsWith('\r\n'))) {
change.newText = change.newText.trimStart();
}
}
else {
const existingLine = (0, utils_1.getLineAtPosition)(document.positionAt(span.start), document.getText());
const isNewImport = !existingLine.trim().startsWith('import');
// Avoid putting new imports on the same line as the script tag opening
if (!(change.newText.startsWith('\n') || change.newText.startsWith('\r\n')) && isNewImport) {
change.newText = ts.sys.newLine + change.newText;
}
}
return vscode_languageserver_1.TextEdit.replace(range, change.newText);
}
exports.codeActionChangeToTextEdit = codeActionChangeToTextEdit;

View file

@ -0,0 +1,9 @@
import { LocationLink, Position } from 'vscode-languageserver-types';
import { AstroDocument } from '../../../core/documents';
import type { DefinitionsProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
export declare class DefinitionsProviderImpl implements DefinitionsProvider {
private languageServiceManager;
constructor(languageServiceManager: LanguageServiceManager);
getDefinitions(document: AstroDocument, position: Position): Promise<LocationLink[]>;
}

View file

@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefinitionsProviderImpl = void 0;
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const documents_1 = require("../../../core/documents");
const utils_1 = require("../../../utils");
const utils_2 = require("../utils");
const utils_3 = require("./utils");
class DefinitionsProviderImpl {
constructor(languageServiceManager) {
this.languageServiceManager = languageServiceManager;
}
async getDefinitions(document, position) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const fragmentPosition = tsDoc.getGeneratedPosition(position);
const fragmentOffset = tsDoc.offsetAt(fragmentPosition);
let defs;
const html = document.html;
const offset = document.offsetAt(position);
const node = html.findNodeAt(offset);
if (node.tag === 'script') {
const { snapshot: scriptTagSnapshot, filePath: scriptFilePath, offset: scriptOffset, } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node, position);
defs = lang.getDefinitionAndBoundSpan(scriptFilePath, scriptOffset);
if (defs) {
defs.definitions = defs.definitions?.map((def) => {
const isInSameFile = def.fileName === scriptFilePath;
def.fileName = isInSameFile ? tsDoc.filePath : def.fileName;
if (isInSameFile) {
def.textSpan.start = (0, documents_1.mapScriptSpanStartToSnapshot)(def.textSpan, scriptTagSnapshot, tsDoc);
}
return def;
});
defs.textSpan.start = (0, documents_1.mapScriptSpanStartToSnapshot)(defs.textSpan, scriptTagSnapshot, tsDoc);
}
}
else {
defs = lang.getDefinitionAndBoundSpan(tsDoc.filePath, fragmentOffset);
}
if (!defs || !defs.definitions) {
return [];
}
const snapshots = new utils_3.SnapshotMap(this.languageServiceManager);
snapshots.set(tsDoc.filePath, tsDoc);
const result = await Promise.all(defs.definitions.map(async (def) => {
const snapshot = await snapshots.retrieve(def.fileName);
const fileName = (0, utils_2.ensureRealFilePath)(def.fileName);
// For Astro, Svelte and Vue, the position is wrongly mapped to the end of the file due to the TSX output
// So we'll instead redirect to the beginning of the file
const isFramework = ((0, utils_2.isFrameworkFilePath)(def.fileName) || (0, utils_2.isAstroFilePath)(def.fileName)) && tsDoc.filePath !== def.fileName;
const targetRange = isFramework
? (0, utils_2.convertRange)(document, { start: 0, length: 0 })
: (0, utils_2.convertToLocationRange)(snapshot, def.textSpan);
return vscode_languageserver_types_1.LocationLink.create((0, utils_1.pathToUrl)(fileName), targetRange, targetRange, (0, utils_2.convertToLocationRange)(tsDoc, defs.textSpan));
}));
return result.filter(utils_1.isNotNullOrUndefined);
}
}
exports.DefinitionsProviderImpl = DefinitionsProviderImpl;

View file

@ -0,0 +1,25 @@
import type ts from 'typescript';
import { CancellationToken, DiagnosticSeverity } from 'vscode-languageserver';
import { Diagnostic } from 'vscode-languageserver-types';
import { AstroDocument } from '../../../core/documents';
import type { DiagnosticsProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
export declare enum DiagnosticCodes {
SPREAD_EXPECTED = 1005,
IS_NOT_A_MODULE = 2306,
CANNOT_FIND_MODULE = 2307,
DUPLICATED_JSX_ATTRIBUTES = 17001,
CANT_RETURN_OUTSIDE_FUNC = 1108,
ISOLATED_MODULE_COMPILE_ERR = 1208,
TYPE_NOT_ASSIGNABLE = 2322,
JSX_NO_CLOSING_TAG = 17008,
JSX_ELEMENT_NO_CALL = 2604
}
export declare class DiagnosticsProviderImpl implements DiagnosticsProvider {
private languageServiceManager;
private ts;
constructor(languageServiceManager: LanguageServiceManager);
getDiagnostics(document: AstroDocument, _cancellationToken?: CancellationToken): Promise<Diagnostic[]>;
private getTagBoundaries;
mapSeverity(category: ts.DiagnosticCategory): DiagnosticSeverity;
}

View file

@ -0,0 +1,249 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticsProviderImpl = exports.DiagnosticCodes = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const documents_1 = require("../../../core/documents");
const utils_1 = require("../utils");
// List of codes:
// https://github.com/Microsoft/TypeScript/blob/main/src/compiler/diagnosticMessages.json
var DiagnosticCodes;
(function (DiagnosticCodes) {
DiagnosticCodes[DiagnosticCodes["SPREAD_EXPECTED"] = 1005] = "SPREAD_EXPECTED";
DiagnosticCodes[DiagnosticCodes["IS_NOT_A_MODULE"] = 2306] = "IS_NOT_A_MODULE";
DiagnosticCodes[DiagnosticCodes["CANNOT_FIND_MODULE"] = 2307] = "CANNOT_FIND_MODULE";
DiagnosticCodes[DiagnosticCodes["DUPLICATED_JSX_ATTRIBUTES"] = 17001] = "DUPLICATED_JSX_ATTRIBUTES";
DiagnosticCodes[DiagnosticCodes["CANT_RETURN_OUTSIDE_FUNC"] = 1108] = "CANT_RETURN_OUTSIDE_FUNC";
DiagnosticCodes[DiagnosticCodes["ISOLATED_MODULE_COMPILE_ERR"] = 1208] = "ISOLATED_MODULE_COMPILE_ERR";
DiagnosticCodes[DiagnosticCodes["TYPE_NOT_ASSIGNABLE"] = 2322] = "TYPE_NOT_ASSIGNABLE";
DiagnosticCodes[DiagnosticCodes["JSX_NO_CLOSING_TAG"] = 17008] = "JSX_NO_CLOSING_TAG";
DiagnosticCodes[DiagnosticCodes["JSX_ELEMENT_NO_CALL"] = 2604] = "JSX_ELEMENT_NO_CALL";
})(DiagnosticCodes = exports.DiagnosticCodes || (exports.DiagnosticCodes = {}));
class DiagnosticsProviderImpl {
constructor(languageServiceManager) {
this.languageServiceManager = languageServiceManager;
this.ts = languageServiceManager.docContext.ts;
}
async getDiagnostics(document, _cancellationToken) {
// Don't return diagnostics for files inside node_modules. These are considered read-only
// and they would pollute the output for astro check
if (document.getFilePath()?.includes('/node_modules/') || document.getFilePath()?.includes('\\node_modules\\')) {
return [];
}
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
// If we have compiler errors, our TSX isn't valid so don't bother showing TS errors
if (tsDoc.isInErrorState) {
return [];
}
let scriptDiagnostics = [];
document.scriptTags.forEach((scriptTag) => {
const { filePath: scriptFilePath, snapshot: scriptTagSnapshot } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
const scriptDiagnostic = [
...lang.getSyntacticDiagnostics(scriptFilePath),
...lang.getSuggestionDiagnostics(scriptFilePath),
...lang.getSemanticDiagnostics(scriptFilePath),
]
// We need to duplicate the diagnostic creation here because we can't map TS's diagnostics range to the original
// file due to some internal cache inside TS that would cause it to being mapped twice in some cases
.map((diagnostic) => ({
range: (0, utils_1.convertRange)(scriptTagSnapshot, diagnostic),
severity: this.mapSeverity(diagnostic.category),
source: 'ts',
message: this.ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
code: diagnostic.code,
tags: getDiagnosticTag(diagnostic),
}))
.map(mapRange(scriptTagSnapshot, document));
scriptDiagnostics.push(...scriptDiagnostic);
});
const { script: scriptBoundaries } = this.getTagBoundaries(lang, tsDoc.filePath);
const diagnostics = [
...lang.getSyntacticDiagnostics(tsDoc.filePath),
...lang.getSuggestionDiagnostics(tsDoc.filePath),
...lang.getSemanticDiagnostics(tsDoc.filePath),
].filter((diag) => {
return isNoWithinBoundary(scriptBoundaries, diag, this.ts);
});
return [
...diagnostics
.map((diagnostic) => ({
range: (0, utils_1.convertRange)(tsDoc, diagnostic),
severity: this.mapSeverity(diagnostic.category),
source: 'ts',
message: this.ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
code: diagnostic.code,
tags: getDiagnosticTag(diagnostic),
}))
.map(mapRange(tsDoc, document)),
...scriptDiagnostics,
]
.filter((diag) => {
return (
// Make sure the diagnostic is inside the document and not in generated code
diag.range.start.line <= document.lineCount &&
hasNoNegativeLines(diag) &&
isNoCantReturnOutsideFunction(diag) &&
isNoIsolatedModuleError(diag) &&
isNoJsxCannotHaveMultipleAttrsError(diag));
})
.map(enhanceIfNecessary);
}
getTagBoundaries(lang, tsFilePath) {
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(tsFilePath);
const boundaries = {
script: [],
};
if (!sourceFile) {
return boundaries;
}
function findTags(parent, ts) {
ts.forEachChild(parent, (node) => {
if (ts.isJsxElement(node)) {
let tagName = node.openingElement.tagName.getText();
switch (tagName) {
case 'script': {
ts.getLineAndCharacterOfPosition(sourceFile, node.getStart());
boundaries.script.push([node.getStart(), node.getEnd()]);
break;
}
}
}
findTags(node, ts);
});
}
findTags(sourceFile, this.ts);
return boundaries;
}
mapSeverity(category) {
switch (category) {
case this.ts.DiagnosticCategory.Error:
return vscode_languageserver_1.DiagnosticSeverity.Error;
case this.ts.DiagnosticCategory.Warning:
return vscode_languageserver_1.DiagnosticSeverity.Warning;
case this.ts.DiagnosticCategory.Suggestion:
return vscode_languageserver_1.DiagnosticSeverity.Hint;
case this.ts.DiagnosticCategory.Message:
return vscode_languageserver_1.DiagnosticSeverity.Information;
default:
return vscode_languageserver_1.DiagnosticSeverity.Error;
}
}
}
exports.DiagnosticsProviderImpl = DiagnosticsProviderImpl;
function isWithinBoundaries(boundaries, start) {
for (let [bstart, bend] of boundaries) {
if (start > bstart && start < bend) {
return true;
}
}
return false;
}
function diagnosticIsWithinBoundaries(sourceFile, boundaries, diagnostic, ts) {
if ('start' in diagnostic) {
if (diagnostic.start == null)
return false;
return isWithinBoundaries(boundaries, diagnostic.start);
}
if (!sourceFile)
return false;
let startRange = diagnostic.range.start;
let pos = ts.getPositionOfLineAndCharacter(sourceFile, startRange.line, startRange.character);
return isWithinBoundaries(boundaries, pos);
}
function isNoWithinBoundary(boundaries, diagnostic, ts) {
return !diagnosticIsWithinBoundaries(undefined, boundaries, diagnostic, ts);
}
function mapRange(snapshot, _document) {
return (diagnostic) => {
let range = (0, documents_1.mapRangeToOriginal)(snapshot, diagnostic.range);
return { ...diagnostic, range };
};
}
/**
* In some rare cases mapping of diagnostics does not work and produces negative lines.
* We filter out these diagnostics with negative lines because else the LSP
* apparently has a hiccup and does not show any diagnostics at all.
*/
function hasNoNegativeLines(diagnostic) {
return diagnostic.range.start.line >= 0 && diagnostic.range.end.line >= 0;
}
/**
* Astro allows multiple attributes to have the same name
*/
function isNoJsxCannotHaveMultipleAttrsError(diagnostic) {
return diagnostic.code !== DiagnosticCodes.DUPLICATED_JSX_ATTRIBUTES;
}
/**
* Ignore "Can't return outside of function body"
* Since the frontmatter is at the top level, users trying to return a Response for SSR mode run into this
*/
function isNoCantReturnOutsideFunction(diagnostic) {
return diagnostic.code !== DiagnosticCodes.CANT_RETURN_OUTSIDE_FUNC;
}
/**
* When the content of the file is invalid and can't be parsed properly for TSX generation, TS will show an error about
* how the current module can't be compiled under --isolatedModule, this is confusing to users so let's ignore this
*/
function isNoIsolatedModuleError(diagnostic) {
return diagnostic.code !== DiagnosticCodes.ISOLATED_MODULE_COMPILE_ERR;
}
/**
* Some diagnostics have JSX-specific nomenclature or unclear description. Enhance them for more clarity.
*/
function enhanceIfNecessary(diagnostic) {
// When the language integrations are not installed, the content of the imported snapshot is empty
// As such, it triggers the "is not a module error", which we can enhance with a more helpful message for the related framework
if (diagnostic.code === DiagnosticCodes.IS_NOT_A_MODULE) {
if (diagnostic.message.includes('.svelte')) {
diagnostic.message +=
'\n\nIs the `@astrojs/svelte` package installed? You can add it to your project by running the following command: `astro add svelte`. If already installed, restarting the language server might be necessary in order for the change to take effect';
}
if (diagnostic.message.includes('.vue')) {
diagnostic.message +=
'\n\nIs the `@astrojs/vue` package installed? You can add it to your project by running the following command: `astro add vue`. If already installed, restarting the language server might be necessary in order for the change to take effect';
}
return diagnostic;
}
if (diagnostic.code === DiagnosticCodes.CANNOT_FIND_MODULE && diagnostic.message.includes('astro:content')) {
diagnostic.message +=
"\n\nIf you're using content collections, make sure to run `astro dev`, `astro build` or `astro sync` to first generate the types so you can import from them. If you already ran one of those commands, restarting the language server might be necessary in order for the change to take effect";
return diagnostic;
}
// JSX element has no closing tag. JSX -> HTML
if (diagnostic.code === DiagnosticCodes.JSX_NO_CLOSING_TAG) {
return {
...diagnostic,
message: diagnostic.message.replace('JSX', 'HTML'),
};
}
// JSX Element can't be constructed or called. This happens on syntax errors / invalid components
if (diagnostic.code === DiagnosticCodes.JSX_ELEMENT_NO_CALL) {
return {
...diagnostic,
message: diagnostic.message
.replace('JSX element type', 'Component')
.replace('does not have any construct or call signatures.', 'is not a valid component.\n\nIf this is a Svelte or Vue component, it might have a syntax error that makes it impossible to parse.'),
};
}
// For the rare case where an user might try to put a client directive on something that is not a component
if (diagnostic.code === DiagnosticCodes.TYPE_NOT_ASSIGNABLE) {
if (diagnostic.message.includes("Property 'client:") && diagnostic.message.includes("to type 'HTMLAttributes")) {
return {
...diagnostic,
message: 'Client directives are only available on framework components',
};
}
}
return diagnostic;
}
function getDiagnosticTag(diagnostic) {
const tags = [];
if (diagnostic.reportsUnnecessary) {
tags.push(vscode_languageserver_types_1.DiagnosticTag.Unnecessary);
}
if (diagnostic.reportsDeprecated) {
tags.push(vscode_languageserver_types_1.DiagnosticTag.Deprecated);
}
return tags;
}

View file

@ -0,0 +1,11 @@
import { SymbolInformation } from 'vscode-languageserver-types';
import { AstroDocument } from '../../../core/documents';
import type { DocumentSymbolsProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
export declare class DocumentSymbolsProviderImpl implements DocumentSymbolsProvider {
private languageServiceManager;
private ts;
constructor(languageServiceManager: LanguageServiceManager);
getDocumentSymbols(document: AstroDocument): Promise<SymbolInformation[]>;
private collectSymbols;
}

View file

@ -0,0 +1,67 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DocumentSymbolsProviderImpl = void 0;
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const utils_1 = require("../utils");
class DocumentSymbolsProviderImpl {
constructor(languageServiceManager) {
this.languageServiceManager = languageServiceManager;
this.ts = languageServiceManager.docContext.ts;
}
async getDocumentSymbols(document) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const navTree = lang.getNavigationTree(tsDoc.filePath + '?documentSymbols');
if (!navTree) {
return [];
}
const symbols = [];
this.collectSymbols(navTree, document, undefined, (symbol) => symbols.push(symbol));
const originalContainerName = symbols[0].name;
const result = [];
// Add a "Frontmatter" namespace for the frontmatter if we have a closed one
if (document.astroMeta.frontmatter.state === 'closed') {
result.push(vscode_languageserver_types_1.SymbolInformation.create('Frontmatter', vscode_languageserver_types_1.SymbolKind.Namespace, vscode_languageserver_types_1.Range.create(document.positionAt(document.astroMeta.frontmatter.startOffset), document.positionAt(document.astroMeta.frontmatter.endOffset)), document.getURL()));
}
// Add a "Template" namespace for everything under the frontmatter
result.push(vscode_languageserver_types_1.SymbolInformation.create('Template', vscode_languageserver_types_1.SymbolKind.Namespace, vscode_languageserver_types_1.Range.create(document.positionAt(document.astroMeta.frontmatter.endOffset ?? 0), document.positionAt(document.getTextLength())), document.getURL()));
for (let symbol of symbols.splice(1)) {
if (document.offsetAt(symbol.location.range.end) >= (document.astroMeta.content.firstNonWhitespaceOffset ?? 0)) {
if (symbol.containerName === originalContainerName) {
symbol.containerName = 'Template';
}
// For some reason, it seems like TypeScript thinks that the "class" attribute is a real class, weird
if (symbol.kind === vscode_languageserver_types_1.SymbolKind.Class && symbol.name === '<class>') {
const node = document.html.findNodeAt(document.offsetAt(symbol.location.range.start));
if (node.attributes?.class) {
continue;
}
}
}
// Remove the exported function in our TSX output from the symbols
if (document.offsetAt(symbol.location.range.start) >= document.getTextLength()) {
continue;
}
result.push(symbol);
}
return result;
}
collectSymbols(item, document, container, cb) {
for (const span of item.spans) {
const symbol = vscode_languageserver_types_1.SymbolInformation.create(item.text, (0, utils_1.symbolKindFromString)(item.kind), vscode_languageserver_types_1.Range.create(document.positionAt(span.start), document.positionAt(span.start + span.length)), document.getURL(), container);
// TypeScript gives us kind modifiers as a string instead of an array
const kindModifiers = new Set(item.kindModifiers.split(/,|\s+/g));
if (kindModifiers.has(this.ts.ScriptElementKindModifier.deprecatedModifier)) {
if (!symbol.tags)
symbol.tags = [];
symbol.tags.push(vscode_languageserver_types_1.SymbolTag.Deprecated);
}
cb(symbol);
}
if (item.childItems) {
for (const child of item.childItems) {
this.collectSymbols(child, document, item.text, cb);
}
}
}
}
exports.DocumentSymbolsProviderImpl = DocumentSymbolsProviderImpl;

View file

@ -0,0 +1,9 @@
import { Location } from 'vscode-languageserver';
import { AstroDocument } from '../../../core/documents';
import { FileReferencesProvider } from '../../interfaces';
import { LanguageServiceManager } from '../LanguageServiceManager';
export declare class FileReferencesProviderImpl implements FileReferencesProvider {
private languageServiceManager;
constructor(languageServiceManager: LanguageServiceManager);
fileReferences(document: AstroDocument): Promise<Location[] | null>;
}

View file

@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileReferencesProviderImpl = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const utils_1 = require("../../../utils");
const utils_2 = require("../utils");
const utils_3 = require("./utils");
class FileReferencesProviderImpl {
constructor(languageServiceManager) {
this.languageServiceManager = languageServiceManager;
}
async fileReferences(document) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const references = lang.getFileReferences(tsDoc.filePath);
if (!references) {
return null;
}
const snapshots = new utils_3.SnapshotMap(this.languageServiceManager);
snapshots.set(tsDoc.filePath, tsDoc);
const locations = await Promise.all(references.map(async (ref) => {
const snapshot = await snapshots.retrieve(ref.fileName);
return vscode_languageserver_1.Location.create((0, utils_1.pathToUrl)(ref.fileName), (0, utils_2.convertToLocationRange)(snapshot, ref.textSpan));
}));
return locations;
}
}
exports.FileReferencesProviderImpl = FileReferencesProviderImpl;

View file

@ -0,0 +1,12 @@
import type ts from 'typescript';
import { FoldingRange } from 'vscode-languageserver';
import type { AstroDocument } from '../../../core/documents';
import type { FoldingRangesProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
export declare class FoldingRangesProviderImpl implements FoldingRangesProvider {
private languageServiceManager;
private ts;
constructor(languageServiceManager: LanguageServiceManager);
getFoldingRanges(document: AstroDocument): Promise<FoldingRange[] | null>;
transformFoldingRangeKind(tsKind: ts.OutliningSpanKind): "imports" | "comment" | "region" | undefined;
}

View file

@ -0,0 +1,71 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FoldingRangesProviderImpl = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const utils_1 = require("../utils");
class FoldingRangesProviderImpl {
constructor(languageServiceManager) {
this.languageServiceManager = languageServiceManager;
this.ts = languageServiceManager.docContext.ts;
}
async getFoldingRanges(document) {
const html = document.html;
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const outliningSpans = lang.getOutliningSpans(tsDoc.filePath).filter((span) => {
const node = html.findNodeAt(span.textSpan.start);
// Due to how our TSX output transform those tags into function calls or template literals
// TypeScript thinks of those as outlining spans, which is fine but we don't want folding ranges for those
return node.tag !== 'script' && node.tag !== 'style';
});
const scriptOutliningSpans = [];
document.scriptTags.forEach((scriptTag) => {
const { snapshot: scriptTagSnapshot, filePath: scriptFilePath } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
scriptOutliningSpans.push(...lang.getOutliningSpans(scriptFilePath).map((span) => {
span.textSpan.start = document.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(span.textSpan.start)));
return span;
}));
});
const foldingRanges = [];
for (const span of [...outliningSpans, ...scriptOutliningSpans]) {
const start = tsDoc.getOriginalPosition(tsDoc.positionAt(span.textSpan.start));
const end = adjustFoldingEnd(start, document.positionAt(document.offsetAt(start) + span.textSpan.length), document);
// When using this method for generating folding ranges, TypeScript tend to return some
// one line / one character ones that we should be able to safely ignore
if (start.line === end.line && start.character === end.character) {
continue;
}
// Ignore folding ranges that are from unmapped regions
if (start.line < 0 || end.line < 0) {
continue;
}
foldingRanges.push(vscode_languageserver_1.FoldingRange.create(start.line, end.line, start.character, end.character, this.transformFoldingRangeKind(span.kind)));
}
return foldingRanges;
}
transformFoldingRangeKind(tsKind) {
switch (tsKind) {
case this.ts.OutliningSpanKind.Comment:
return vscode_languageserver_1.FoldingRangeKind.Comment;
case this.ts.OutliningSpanKind.Imports:
return vscode_languageserver_1.FoldingRangeKind.Imports;
case this.ts.OutliningSpanKind.Region:
return vscode_languageserver_1.FoldingRangeKind.Region;
}
}
}
exports.FoldingRangesProviderImpl = FoldingRangesProviderImpl;
// https://github.com/microsoft/vscode/blob/bed61166fb604e519e82e4d1d1ed839bc45d65f8/extensions/typescript-language-features/src/languageFeatures/folding.ts#L61-L73
function adjustFoldingEnd(start, end, document) {
// workaround for #47240
if (end.character > 0) {
const foldEndCharacter = document.getText({
start: { line: end.line, character: end.character - 1 },
end,
});
if (['}', ']', ')', '`'].includes(foldEndCharacter)) {
const endOffset = Math.max(document.offsetAt({ line: end.line, character: 0 }) - 1, document.offsetAt(start));
return document.positionAt(endOffset);
}
}
return end;
}

View file

@ -0,0 +1,10 @@
import type { Hover, Position } from 'vscode-languageserver';
import { AstroDocument } from '../../../core/documents';
import type { HoverProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
export declare class HoverProviderImpl implements HoverProvider {
private languageServiceManager;
private ts;
constructor(languageServiceManager: LanguageServiceManager);
doHover(document: AstroDocument, position: Position): Promise<Hover | null>;
}

View file

@ -0,0 +1,50 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HoverProviderImpl = void 0;
const documents_1 = require("../../../core/documents");
const previewer_1 = require("../previewer");
const utils_1 = require("../utils");
const partsMap = new Map([['JSX attribute', 'HTML attribute']]);
class HoverProviderImpl {
constructor(languageServiceManager) {
this.languageServiceManager = languageServiceManager;
this.ts = languageServiceManager.docContext.ts;
}
async doHover(document, position) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
const html = document.html;
const documentOffset = document.offsetAt(position);
const node = html.findNodeAt(documentOffset);
let info;
if (node.tag === 'script') {
const { snapshot: scriptTagSnapshot, filePath: scriptFilePath, offset: scriptOffset, } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, node, position);
info = lang.getQuickInfoAtPosition(scriptFilePath, scriptOffset);
if (info) {
info.textSpan.start = (0, documents_1.mapScriptSpanStartToSnapshot)(info.textSpan, scriptTagSnapshot, tsDoc);
}
}
else {
info = lang.getQuickInfoAtPosition(tsDoc.filePath, offset);
}
if (!info) {
return null;
}
const textSpan = info.textSpan;
const displayParts = (info.displayParts || []).map((value) => ({
text: partsMap.has(value.text) ? partsMap.get(value.text) : value.text,
kind: value.kind,
}));
const declaration = this.ts.displayPartsToString(displayParts);
const documentation = (0, previewer_1.getMarkdownDocumentation)(info.documentation, info.tags, this.ts);
// https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
const contents = ['```typescript', declaration, '```']
.concat(documentation ? ['---', documentation] : [])
.join('\n');
return (0, documents_1.mapObjWithRangeToOriginal)(tsDoc, {
range: (0, utils_1.convertRange)(tsDoc, textSpan),
contents,
});
}
}
exports.HoverProviderImpl = HoverProviderImpl;

View file

@ -0,0 +1,9 @@
import { Location, Position } from 'vscode-languageserver-types';
import { AstroDocument } from '../../../core/documents';
import { ImplementationProvider } from '../../interfaces';
import { LanguageServiceManager } from '../LanguageServiceManager';
export declare class ImplementationsProviderImpl implements ImplementationProvider {
private languageServiceManager;
constructor(languageServiceManager: LanguageServiceManager);
getImplementation(document: AstroDocument, position: Position): Promise<Location[] | null>;
}

View file

@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImplementationsProviderImpl = void 0;
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const documents_1 = require("../../../core/documents");
const utils_1 = require("../../../utils");
const utils_2 = require("../utils");
const utils_3 = require("./utils");
class ImplementationsProviderImpl {
constructor(languageServiceManager) {
this.languageServiceManager = languageServiceManager;
}
async getImplementation(document, position) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const fragmentPosition = tsDoc.getGeneratedPosition(position);
const fragmentOffset = tsDoc.offsetAt(fragmentPosition);
const html = document.html;
const offset = document.offsetAt(position);
const node = document.html.findNodeAt(offset);
let implementations;
if (node.tag === 'script') {
const { snapshot: scriptTagSnapshot, filePath: scriptFilePath, offset: scriptOffset, } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node, position);
implementations = lang.getImplementationAtPosition(scriptFilePath, scriptOffset);
if (implementations) {
implementations = implementations.map((impl) => {
const isInSameFile = impl.fileName === scriptFilePath;
impl.fileName = isInSameFile ? tsDoc.filePath : impl.fileName;
if (isInSameFile) {
impl.textSpan.start = (0, documents_1.mapScriptSpanStartToSnapshot)(impl.textSpan, scriptTagSnapshot, tsDoc);
}
return impl;
});
}
}
else {
implementations = lang.getImplementationAtPosition(tsDoc.filePath, fragmentOffset);
}
const snapshots = new utils_3.SnapshotMap(this.languageServiceManager);
snapshots.set(tsDoc.filePath, tsDoc);
if (!implementations) {
return null;
}
const result = await Promise.all(implementations.map(async (implementation) => {
const snapshot = await snapshots.retrieve(implementation.fileName);
const range = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, implementation.textSpan));
if (range.start.line >= 0 && range.end.line >= 0) {
return vscode_languageserver_types_1.Location.create((0, utils_1.pathToUrl)(implementation.fileName), range);
}
}));
return result.filter(utils_1.isNotNullOrUndefined);
}
}
exports.ImplementationsProviderImpl = ImplementationsProviderImpl;

View file

@ -0,0 +1,13 @@
import { InlayHint } from 'vscode-languageserver';
import { Range } from 'vscode-languageserver-types';
import type { ConfigManager } from '../../../core/config';
import type { AstroDocument } from '../../../core/documents';
import type { InlayHintsProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
export declare class InlayHintsProviderImpl implements InlayHintsProvider {
private languageServiceManager;
private configManager;
private ts;
constructor(languageServiceManager: LanguageServiceManager, configManager: ConfigManager);
getInlayHints(document: AstroDocument, range: Range): Promise<InlayHint[]>;
}

View file

@ -0,0 +1,30 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InlayHintsProviderImpl = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
class InlayHintsProviderImpl {
constructor(languageServiceManager, configManager) {
this.languageServiceManager = languageServiceManager;
this.configManager = configManager;
this.ts = languageServiceManager.docContext.ts;
}
async getInlayHints(document, range) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.start));
const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.end));
const tsPreferences = await this.configManager.getTSPreferences(document);
const inlayHints = lang.provideInlayHints(tsDoc.filePath, { start, length: end - start }, tsPreferences);
return inlayHints.map((hint) => {
const result = vscode_languageserver_1.InlayHint.create(tsDoc.getOriginalPosition(tsDoc.positionAt(hint.position)), hint.text, hint.kind === this.ts.InlayHintKind.Type
? vscode_languageserver_types_1.InlayHintKind.Type
: hint.kind === this.ts.InlayHintKind.Parameter
? vscode_languageserver_types_1.InlayHintKind.Parameter
: undefined);
result.paddingLeft = hint.whitespaceBefore;
result.paddingRight = hint.whitespaceAfter;
return result;
});
}
}
exports.InlayHintsProviderImpl = InlayHintsProviderImpl;

View file

@ -0,0 +1,9 @@
import { Location, Position, ReferenceContext } from 'vscode-languageserver-types';
import { AstroDocument } from '../../../core/documents';
import type { FindReferencesProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
export declare class FindReferencesProviderImpl implements FindReferencesProvider {
private languageServiceManager;
constructor(languageServiceManager: LanguageServiceManager);
findReferences(document: AstroDocument, position: Position, context: ReferenceContext): Promise<Location[] | null>;
}

View file

@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FindReferencesProviderImpl = void 0;
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const documents_1 = require("../../../core/documents");
const utils_1 = require("../../../utils");
const utils_2 = require("../utils");
const utils_3 = require("./utils");
class FindReferencesProviderImpl {
constructor(languageServiceManager) {
this.languageServiceManager = languageServiceManager;
}
async findReferences(document, position, context) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const fragmentPosition = tsDoc.getGeneratedPosition(position);
const fragmentOffset = tsDoc.offsetAt(fragmentPosition);
const offset = document.offsetAt(position);
const node = document.html.findNodeAt(offset);
let references;
if (node.tag === 'script') {
const { snapshot: scriptTagSnapshot, filePath: scriptFilePath, offset: scriptOffset, } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node, position);
references = lang.getReferencesAtPosition(scriptFilePath, scriptOffset);
if (references) {
references = references.map((ref) => {
const isInSameFile = ref.fileName === scriptFilePath;
ref.fileName = isInSameFile ? tsDoc.filePath : ref.fileName;
if (isInSameFile) {
ref.textSpan.start = (0, documents_1.mapScriptSpanStartToSnapshot)(ref.textSpan, scriptTagSnapshot, tsDoc);
}
return ref;
});
}
}
else {
references = lang.getReferencesAtPosition(tsDoc.filePath, fragmentOffset);
}
if (!references) {
return null;
}
const snapshots = new utils_3.SnapshotMap(this.languageServiceManager);
snapshots.set(tsDoc.filePath, tsDoc);
const result = await Promise.all(references.map(async (reference) => {
if (!context.includeDeclaration) {
return null;
}
const snapshot = await snapshots.retrieve(reference.fileName);
const range = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, reference.textSpan));
if (range.start.line >= 0 && range.end.line >= 0) {
return vscode_languageserver_types_1.Location.create((0, utils_1.pathToUrl)(reference.fileName), range);
}
}));
return result.filter(utils_1.isNotNullOrUndefined);
}
}
exports.FindReferencesProviderImpl = FindReferencesProviderImpl;

View file

@ -0,0 +1,13 @@
import type { Position, Range, WorkspaceEdit } from 'vscode-languageserver-types';
import type { ConfigManager } from '../../../core/config';
import { AstroDocument } from '../../../core/documents';
import type { RenameProvider } from '../../interfaces';
import type { LanguageServiceManager } from '../LanguageServiceManager';
export declare class RenameProviderImpl implements RenameProvider {
private languageServiceManager;
private configManager;
private ts;
constructor(languageServiceManager: LanguageServiceManager, configManager: ConfigManager);
prepareRename(document: AstroDocument, position: Position): Promise<Range | null>;
rename(document: AstroDocument, position: Position, newName: string): Promise<WorkspaceEdit | null>;
}

View file

@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RenameProviderImpl = void 0;
const documents_1 = require("../../../core/documents");
const utils_1 = require("../../../utils");
const utils_2 = require("../utils");
const utils_3 = require("./utils");
class RenameProviderImpl {
constructor(languageServiceManager, configManager) {
this.languageServiceManager = languageServiceManager;
this.configManager = configManager;
this.ts = languageServiceManager.docContext.ts;
}
async prepareRename(document, position) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
// If our TSX isn't valid, we can't rename safely, so let's abort
if (tsDoc.isInErrorState) {
return null;
}
// TODO: Allow renaming of import paths
// This requires a bit of work, because we need to create files for the new import paths
const renameInfo = lang.getRenameInfo(tsDoc.filePath, offset, { allowRenameOfImportPath: false });
if (!renameInfo.canRename) {
return null;
}
return (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, renameInfo.triggerSpan));
}
async rename(document, position, newName) {
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
const { providePrefixAndSuffixTextForRename } = await this.configManager.getTSPreferences(document);
let renames = lang.findRenameLocations(tsDoc.filePath, offset, false, false, providePrefixAndSuffixTextForRename);
if (!renames) {
return null;
}
const docs = new utils_3.SnapshotMap(this.languageServiceManager);
docs.set(tsDoc.filePath, tsDoc);
const mappedRenames = await Promise.all(renames.map(async (rename) => {
const snapshot = await docs.retrieve(rename.fileName);
return {
...rename,
range: (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, rename.textSpan)),
newName,
};
}));
return mappedRenames.reduce((acc, loc) => {
const uri = (0, utils_1.pathToUrl)(loc.fileName);
if (!acc.changes[uri]) {
acc.changes[uri] = [];
}
acc.changes[uri].push({
newText: (loc.prefixText || '') + (loc.newName || newName) + (loc.suffixText || ''),
range: loc.range,
});
return acc;
}, { changes: {} });
}
}
exports.RenameProviderImpl = RenameProviderImpl;

Some files were not shown because too many files have changed in this diff Show more