🎉 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

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;