Adds CSS completions to VSCode extension (#214)
* Start on css completion * Support for CSS completions
This commit is contained in:
parent
27a7986a38
commit
c3c96bf498
14 changed files with 1135 additions and 5 deletions
|
@ -19,7 +19,9 @@
|
|||
"astro-scripts": "0.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": "^0.7.3",
|
||||
"typescript": "^4.3.1-rc",
|
||||
"vscode-css-languageservice": "^5.1.1",
|
||||
"vscode-emmet-helper": "2.1.2",
|
||||
"vscode-html-languageservice": "^3.0.3",
|
||||
"vscode-languageserver": "6.1.1",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { TagInformation } from './utils';
|
||||
import { Position, Range } from 'vscode-languageserver';
|
||||
import { TextDocument } from 'vscode-languageserver-textdocument';
|
||||
import { HTMLDocument } from 'vscode-html-languageservice';
|
||||
|
@ -5,6 +6,7 @@ import { HTMLDocument } from 'vscode-html-languageservice';
|
|||
import { clamp, urlToPath } from '../../utils';
|
||||
import { parseHtml } from './parseHtml';
|
||||
import { parseAstro, AstroDocument } from './parseAstro';
|
||||
import { extractStyleTag } from './utils';
|
||||
|
||||
export class Document implements TextDocument {
|
||||
private content: string;
|
||||
|
@ -13,6 +15,7 @@ export class Document implements TextDocument {
|
|||
version = 0;
|
||||
html!: HTMLDocument;
|
||||
astro!: AstroDocument;
|
||||
styleInfo: TagInformation | null = null;
|
||||
|
||||
constructor(public uri: string, text: string) {
|
||||
this.content = text;
|
||||
|
@ -22,6 +25,10 @@ export class Document implements TextDocument {
|
|||
private updateDocInfo() {
|
||||
this.html = parseHtml(this.content);
|
||||
this.astro = parseAstro(this.content);
|
||||
this.styleInfo = extractStyleTag(this.content, this.html);
|
||||
if(this.styleInfo) {
|
||||
this.styleInfo.attributes.lang = 'css';
|
||||
}
|
||||
}
|
||||
|
||||
setText(text: string) {
|
||||
|
|
144
tools/astro-languageserver/src/core/documents/DocumentBase.ts
Normal file
144
tools/astro-languageserver/src/core/documents/DocumentBase.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
import { clamp } from '../../utils';
|
||||
import { Position, TextDocument } from 'vscode-languageserver';
|
||||
|
||||
/**
|
||||
* Represents a textual document.
|
||||
*/
|
||||
export 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.
|
||||
*/
|
||||
public version = 0;
|
||||
|
||||
/**
|
||||
* Get the length of the document's content
|
||||
*/
|
||||
getTextLength(): number {
|
||||
return this.getText().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line and character based on the offset
|
||||
* @param offset The index of the position
|
||||
*/
|
||||
positionAt(offset: number): Position {
|
||||
offset = clamp(offset, 0, this.getTextLength());
|
||||
|
||||
const lineOffsets = this.getLineOffsets();
|
||||
let low = 0;
|
||||
let high = lineOffsets.length;
|
||||
if (high === 0) {
|
||||
return Position.create(0, offset);
|
||||
}
|
||||
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
if (lineOffsets[mid] > offset) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = 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 Position.create(line, offset - lineOffsets[line]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the line and character position
|
||||
* @param position Line and character position
|
||||
*/
|
||||
offsetAt(position: Position): number {
|
||||
const lineOffsets = this.getLineOffsets();
|
||||
|
||||
if (position.line >= lineOffsets.length) {
|
||||
return this.getTextLength();
|
||||
} else if (position.line < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const lineOffset = lineOffsets[position.line];
|
||||
const nextLineOffset =
|
||||
position.line + 1 < lineOffsets.length
|
||||
? lineOffsets[position.line + 1]
|
||||
: this.getTextLength();
|
||||
|
||||
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
|
||||
}
|
||||
|
||||
private getLineOffsets() {
|
||||
const lineOffsets = [];
|
||||
const text = this.getText();
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements TextDocument
|
||||
*/
|
||||
get uri(): string {
|
||||
return this.getURL();
|
||||
}
|
||||
|
||||
get lineCount(): number {
|
||||
return this.getText().split(/\r?\n/).length;
|
||||
}
|
||||
|
||||
abstract languageId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a textual document that can be manipulated.
|
||||
*/
|
||||
export abstract class WritableDocument extends ReadableDocument {
|
||||
/**
|
||||
* Set the text content of the document
|
||||
* @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 {
|
||||
const content = this.getText();
|
||||
this.setText(content.slice(0, start) + text + content.slice(end));
|
||||
}
|
||||
}
|
377
tools/astro-languageserver/src/core/documents/DocumentMapper.ts
Normal file
377
tools/astro-languageserver/src/core/documents/DocumentMapper.ts
Normal file
|
@ -0,0 +1,377 @@
|
|||
import {
|
||||
Position,
|
||||
Range,
|
||||
CompletionItem,
|
||||
Hover,
|
||||
Diagnostic,
|
||||
ColorPresentation,
|
||||
SymbolInformation,
|
||||
LocationLink,
|
||||
TextDocumentEdit,
|
||||
CodeAction,
|
||||
SelectionRange,
|
||||
TextEdit,
|
||||
InsertReplaceEdit
|
||||
} from 'vscode-languageserver';
|
||||
import { TagInformation, offsetAt, positionAt } from './utils';
|
||||
import { SourceMapConsumer } from 'source-map';
|
||||
|
||||
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 class IdentityMapper implements DocumentMapper {
|
||||
constructor(private url: string, private parent?: DocumentMapper) {}
|
||||
|
||||
getOriginalPosition(generatedPosition: Position): Position {
|
||||
if (this.parent) {
|
||||
generatedPosition = this.getOriginalPosition(generatedPosition);
|
||||
}
|
||||
|
||||
return generatedPosition;
|
||||
}
|
||||
|
||||
getGeneratedPosition(originalPosition: Position): Position {
|
||||
if (this.parent) {
|
||||
originalPosition = this.getGeneratedPosition(originalPosition);
|
||||
}
|
||||
|
||||
return originalPosition;
|
||||
}
|
||||
|
||||
isInGenerated(position: Position): boolean {
|
||||
if (this.parent && !this.parent.isInGenerated(position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getURL(): string {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.parent?.destroy?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps positions in a fragment relative to a parent.
|
||||
*/
|
||||
export class FragmentMapper implements DocumentMapper {
|
||||
constructor(
|
||||
private originalText: string,
|
||||
private tagInfo: TagInformation,
|
||||
private url: string
|
||||
) {}
|
||||
|
||||
getOriginalPosition(generatedPosition: Position): Position {
|
||||
const parentOffset = this.offsetInParent(offsetAt(generatedPosition, this.tagInfo.content));
|
||||
return positionAt(parentOffset, this.originalText);
|
||||
}
|
||||
|
||||
private offsetInParent(offset: number): number {
|
||||
return this.tagInfo.start + offset;
|
||||
}
|
||||
|
||||
getGeneratedPosition(originalPosition: Position): Position {
|
||||
const fragmentOffset = offsetAt(originalPosition, this.originalText) - this.tagInfo.start;
|
||||
return positionAt(fragmentOffset, this.tagInfo.content);
|
||||
}
|
||||
|
||||
isInGenerated(pos: Position): boolean {
|
||||
const offset = offsetAt(pos, this.originalText);
|
||||
return offset >= this.tagInfo.start && offset <= this.tagInfo.end;
|
||||
}
|
||||
|
||||
getURL(): string {
|
||||
return this.url;
|
||||
}
|
||||
}
|
||||
|
||||
export class SourceMapDocumentMapper implements DocumentMapper {
|
||||
constructor(
|
||||
protected consumer: SourceMapConsumer,
|
||||
protected sourceUri: string,
|
||||
private parent?: DocumentMapper
|
||||
) {}
|
||||
|
||||
getOriginalPosition(generatedPosition: Position): Position {
|
||||
if (this.parent) {
|
||||
generatedPosition = this.parent.getOriginalPosition(generatedPosition);
|
||||
}
|
||||
|
||||
if (generatedPosition.line < 0) {
|
||||
return { line: -1, character: -1 };
|
||||
}
|
||||
|
||||
const mapped = this.consumer.originalPositionFor({
|
||||
line: generatedPosition.line + 1,
|
||||
column: generatedPosition.character
|
||||
});
|
||||
|
||||
if (!mapped) {
|
||||
return { line: -1, character: -1 };
|
||||
}
|
||||
|
||||
if (mapped.line === 0) {
|
||||
console.log('Got 0 mapped line from', generatedPosition, 'col was', mapped.column);
|
||||
}
|
||||
|
||||
return {
|
||||
line: (mapped.line || 0) - 1,
|
||||
character: mapped.column || 0
|
||||
};
|
||||
}
|
||||
|
||||
getGeneratedPosition(originalPosition: Position): Position {
|
||||
if (this.parent) {
|
||||
originalPosition = this.parent.getGeneratedPosition(originalPosition);
|
||||
}
|
||||
|
||||
const mapped = this.consumer.generatedPositionFor({
|
||||
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: Position): boolean {
|
||||
if (this.parent && !this.isInGenerated(position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const generated = this.getGeneratedPosition(position);
|
||||
return generated.line >= 0;
|
||||
}
|
||||
|
||||
getURL(): string {
|
||||
return this.sourceUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be called when source mapper is no longer needed in order to prevent memory leaks.
|
||||
*/
|
||||
destroy() {
|
||||
this.parent?.destroy?.();
|
||||
this.consumer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export function mapRangeToOriginal(
|
||||
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
|
||||
range: Range
|
||||
): 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;
|
||||
}
|
||||
|
||||
export function mapRangeToGenerated(fragment: DocumentMapper, range: Range): Range {
|
||||
return Range.create(
|
||||
fragment.getGeneratedPosition(range.start),
|
||||
fragment.getGeneratedPosition(range.end)
|
||||
);
|
||||
}
|
||||
|
||||
export function mapCompletionItemToOriginal(
|
||||
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
|
||||
item: CompletionItem
|
||||
): CompletionItem {
|
||||
if (!item.textEdit) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
textEdit: mapEditToOriginal(fragment, item.textEdit)
|
||||
};
|
||||
}
|
||||
|
||||
export function mapHoverToParent(
|
||||
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
|
||||
hover: Hover
|
||||
): Hover {
|
||||
if (!hover.range) {
|
||||
return hover;
|
||||
}
|
||||
|
||||
return { ...hover, range: mapRangeToOriginal(fragment, hover.range) };
|
||||
}
|
||||
|
||||
export function mapObjWithRangeToOriginal<T extends { range: Range }>(
|
||||
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
|
||||
objWithRange: T
|
||||
): T {
|
||||
return { ...objWithRange, range: mapRangeToOriginal(fragment, objWithRange.range) };
|
||||
}
|
||||
|
||||
export function mapInsertReplaceEditToOriginal(
|
||||
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
|
||||
edit: InsertReplaceEdit
|
||||
): InsertReplaceEdit {
|
||||
return {
|
||||
...edit,
|
||||
insert: mapRangeToOriginal(fragment, edit.insert),
|
||||
replace: mapRangeToOriginal(fragment, edit.replace)
|
||||
};
|
||||
}
|
||||
|
||||
export function mapEditToOriginal(
|
||||
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
|
||||
edit: TextEdit | InsertReplaceEdit
|
||||
): TextEdit | InsertReplaceEdit {
|
||||
return TextEdit.is(edit)
|
||||
? mapObjWithRangeToOriginal(fragment, edit)
|
||||
: mapInsertReplaceEditToOriginal(fragment, edit);
|
||||
}
|
||||
|
||||
export function mapDiagnosticToGenerated(
|
||||
fragment: DocumentMapper,
|
||||
diagnostic: Diagnostic
|
||||
): Diagnostic {
|
||||
return { ...diagnostic, range: mapRangeToGenerated(fragment, diagnostic.range) };
|
||||
}
|
||||
|
||||
export function mapColorPresentationToOriginal(
|
||||
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
|
||||
presentation: ColorPresentation
|
||||
): ColorPresentation {
|
||||
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;
|
||||
}
|
||||
|
||||
export function mapSymbolInformationToOriginal(
|
||||
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
|
||||
info: SymbolInformation
|
||||
): SymbolInformation {
|
||||
return { ...info, location: mapObjWithRangeToOriginal(fragment, info.location) };
|
||||
}
|
||||
|
||||
export function mapLocationLinkToOriginal(
|
||||
fragment: DocumentMapper,
|
||||
def: LocationLink
|
||||
): LocationLink {
|
||||
return 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
|
||||
);
|
||||
}
|
||||
|
||||
export function mapTextDocumentEditToOriginal(fragment: DocumentMapper, edit: TextDocumentEdit) {
|
||||
if (edit.textDocument.uri !== fragment.getURL()) {
|
||||
return edit;
|
||||
}
|
||||
|
||||
return TextDocumentEdit.create(
|
||||
edit.textDocument,
|
||||
edit.edits.map((textEdit) => mapObjWithRangeToOriginal(fragment, textEdit))
|
||||
);
|
||||
}
|
||||
|
||||
export function mapCodeActionToOriginal(fragment: DocumentMapper, codeAction: CodeAction) {
|
||||
return CodeAction.create(
|
||||
codeAction.title,
|
||||
{
|
||||
documentChanges: codeAction.edit!.documentChanges!.map((edit) =>
|
||||
mapTextDocumentEditToOriginal(fragment, edit as TextDocumentEdit)
|
||||
)
|
||||
},
|
||||
codeAction.kind
|
||||
);
|
||||
}
|
||||
|
||||
export function mapSelectionRangeToParent(
|
||||
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
|
||||
selectionRange: SelectionRange
|
||||
): SelectionRange {
|
||||
const { range, parent } = selectionRange;
|
||||
|
||||
return SelectionRange.create(
|
||||
mapRangeToOriginal(fragment, range),
|
||||
parent && mapSelectionRangeToParent(fragment, parent)
|
||||
);
|
||||
}
|
|
@ -1,2 +1,5 @@
|
|||
export * from './Document';
|
||||
export * from './DocumentBase';
|
||||
export * from './DocumentManager';
|
||||
export * from './DocumentMapper';
|
||||
export * from './utils';
|
|
@ -1,5 +1,41 @@
|
|||
import { Position } from 'vscode-html-languageservice';
|
||||
import { HTMLDocument, Node, Position } from 'vscode-html-languageservice';
|
||||
import { clamp } from '../../utils';
|
||||
import {parseHtml} from './parseHtml';
|
||||
|
||||
export interface TagInformation {
|
||||
content: string;
|
||||
attributes: Record<string, string>;
|
||||
start: number;
|
||||
end: number;
|
||||
startPos: Position;
|
||||
endPos: Position;
|
||||
container: { start: number; end: number };
|
||||
}
|
||||
|
||||
function parseAttributes(
|
||||
rawAttrs: Record<string, string | null> | undefined
|
||||
): Record<string, string> {
|
||||
const attrs: Record<string, string> = {};
|
||||
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: string) {
|
||||
if (
|
||||
(attrValue.startsWith('"') && attrValue.endsWith('"')) ||
|
||||
(attrValue.startsWith("'") && attrValue.endsWith("'"))
|
||||
) {
|
||||
return attrValue.slice(1, attrValue.length - 1);
|
||||
}
|
||||
return attrValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets word range at position.
|
||||
|
@ -125,3 +161,95 @@ function getLineOffsets(text: string) {
|
|||
|
||||
return lineOffsets;
|
||||
}
|
||||
|
||||
export function* walk(node: Node): Generator<Node, void, unknown> {
|
||||
for(let child of node.children) {
|
||||
yield * walk(child);
|
||||
}
|
||||
yield node;
|
||||
}
|
||||
|
||||
/*
|
||||
export function* walk(node: Node, startIndex = 0) {
|
||||
let skip, tmp;
|
||||
let depth = 0;
|
||||
let index = startIndex;
|
||||
|
||||
// Always start with the initial element.
|
||||
do {
|
||||
if ( !skip && (tmp = node.firstChild) ) {
|
||||
depth++;
|
||||
callback('child', node, tmp, index);
|
||||
index++;
|
||||
} else if ( tmp = node.nextSibling ) {
|
||||
skip = false;
|
||||
callback('sibling', node, tmp, index);
|
||||
index++;
|
||||
} else {
|
||||
tmp = node.parentNode;
|
||||
depth--;
|
||||
skip = true;
|
||||
}
|
||||
node = tmp;
|
||||
} while ( depth > 0 );
|
||||
};
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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: string,
|
||||
tag: 'script' | 'style' | 'template',
|
||||
html?: HTMLDocument
|
||||
): TagInformation[] {
|
||||
const rootNodes = html?.roots || parseHtml(text).roots;
|
||||
const matchedNodes = rootNodes
|
||||
.filter((node) => node.tag === tag);
|
||||
|
||||
if(tag === 'style' && !matchedNodes.length && rootNodes.length && rootNodes[0].tag === 'html') {
|
||||
for(let child of walk(rootNodes[0])) {
|
||||
if(child.tag === 'style') {
|
||||
matchedNodes.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchedNodes.map(transformToTagInfo);
|
||||
|
||||
function transformToTagInfo(matchedNode: Node) {
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function extractStyleTag(source: string, html?: HTMLDocument): TagInformation | null {
|
||||
const styles = extractTags(source, 'style', html);
|
||||
if (!styles.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// There can only be one style tag
|
||||
return styles[0];
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { RequestType, TextDocumentPositionParams, createConnection, ProposedFeatures, TextDocumentSyncKind, TextDocumentIdentifier } from 'vscode-languageserver';
|
||||
import { Document, DocumentManager } from './core/documents';
|
||||
import { ConfigManager } from './core/config';
|
||||
import { PluginHost, HTMLPlugin, TypeScriptPlugin, AppCompletionItem, AstroPlugin } from './plugins';
|
||||
import { PluginHost, CSSPlugin, HTMLPlugin, TypeScriptPlugin, AppCompletionItem, AstroPlugin } from './plugins';
|
||||
import { urlToPath } from './utils';
|
||||
|
||||
const TagCloseRequest: RequestType<TextDocumentPositionParams, string | null, any> = new RequestType('html/tag');
|
||||
|
@ -21,6 +21,7 @@ export function startServer() {
|
|||
|
||||
pluginHost.register(new AstroPlugin(docManager, configManager));
|
||||
pluginHost.register(new HTMLPlugin(docManager, configManager));
|
||||
pluginHost.register(new CSSPlugin(docManager, configManager));
|
||||
pluginHost.register(new TypeScriptPlugin(docManager, configManager, workspaceUris));
|
||||
configManager.updateEmmetConfig(evt.initializationOptions?.configuration?.emmet || evt.initializationOptions?.emmetConfig || {});
|
||||
|
||||
|
|
95
tools/astro-languageserver/src/plugins/css/CSSDocument.ts
Normal file
95
tools/astro-languageserver/src/plugins/css/CSSDocument.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { Stylesheet, TextDocument } from 'vscode-css-languageservice';
|
||||
import { Position } from 'vscode-languageserver';
|
||||
import { getLanguageService } from './service';
|
||||
import { Document, DocumentMapper, ReadableDocument, TagInformation } from '../../core/documents/index';
|
||||
|
||||
export interface CSSDocumentBase extends DocumentMapper, TextDocument {
|
||||
languageId: string;
|
||||
stylesheet: Stylesheet;
|
||||
}
|
||||
|
||||
export class CSSDocument extends ReadableDocument implements DocumentMapper {
|
||||
private styleInfo: Pick<TagInformation, 'attributes' | 'start' | 'end'>;
|
||||
readonly version = this.parent.version;
|
||||
|
||||
public stylesheet: Stylesheet;
|
||||
public languageId: string;
|
||||
|
||||
constructor(private parent: Document) {
|
||||
super();
|
||||
|
||||
if (this.parent.styleInfo) {
|
||||
this.styleInfo = this.parent.styleInfo;
|
||||
} else {
|
||||
this.styleInfo = {
|
||||
attributes: {},
|
||||
start: -1,
|
||||
end: -1
|
||||
};
|
||||
}
|
||||
|
||||
this.languageId = this.language;
|
||||
this.stylesheet = getLanguageService(this.language).parseStylesheet(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fragment position relative to the parent
|
||||
* @param pos Position in fragment
|
||||
*/
|
||||
getOriginalPosition(pos: Position): Position {
|
||||
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: Position): Position {
|
||||
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: Position): boolean {
|
||||
const offset = this.parent.offsetAt(pos);
|
||||
return offset >= this.styleInfo.start && offset <= this.styleInfo.end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fragment text from the parent
|
||||
*/
|
||||
getText(): string {
|
||||
return this.parent.getText().slice(this.styleInfo.start, this.styleInfo.end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the fragment as calculated from the start and end positon
|
||||
*/
|
||||
getTextLength(): number {
|
||||
return this.styleInfo.end - this.styleInfo.start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the parent file path
|
||||
*/
|
||||
getFilePath(): string | null {
|
||||
return this.parent.getFilePath();
|
||||
}
|
||||
|
||||
getURL() {
|
||||
return this.parent.getURL();
|
||||
}
|
||||
|
||||
getAttributes() {
|
||||
return this.styleInfo.attributes;
|
||||
}
|
||||
|
||||
private get language() {
|
||||
const attrs = this.getAttributes();
|
||||
return attrs.lang || attrs.type || 'css';
|
||||
}
|
||||
}
|
153
tools/astro-languageserver/src/plugins/css/CSSPlugin.ts
Normal file
153
tools/astro-languageserver/src/plugins/css/CSSPlugin.ts
Normal file
|
@ -0,0 +1,153 @@
|
|||
import type { CompletionsProvider } from '../interfaces';
|
||||
import type { Document, DocumentManager } from '../../core/documents';
|
||||
import type { ConfigManager } from '../../core/config';
|
||||
import { getEmmetCompletionParticipants, doComplete as doEmmetComplete } from 'vscode-emmet-helper';
|
||||
import { CompletionContext, CompletionList, CompletionTriggerKind, Position } from 'vscode-languageserver';
|
||||
import { isInsideFrontmatter } from '../../core/documents/utils';
|
||||
import { CSSDocument, CSSDocumentBase } from './CSSDocument';
|
||||
import { getLanguage, getLanguageService } from './service';
|
||||
import { StyleAttributeDocument } from './StyleAttributeDocument';
|
||||
import { mapCompletionItemToOriginal } from '../../core/documents';
|
||||
import { AttributeContext, getAttributeContextAtPosition } from '../../core/documents/parseHtml';
|
||||
import { getIdClassCompletion } from './features/getIdClassCompletion';
|
||||
|
||||
export class CSSPlugin implements CompletionsProvider {
|
||||
private docManager: DocumentManager;
|
||||
private configManager: ConfigManager;
|
||||
private documents = new WeakMap<Document, CSSDocument>();
|
||||
private triggerCharacters = new Set(['.', ':', '-', '/']);
|
||||
|
||||
constructor(docManager: DocumentManager, configManager: ConfigManager) {
|
||||
this.docManager = docManager;
|
||||
this.configManager = configManager;
|
||||
|
||||
this.docManager.on('documentChange', (document) => {
|
||||
this.documents.set(document, new CSSDocument(document));
|
||||
});
|
||||
}
|
||||
|
||||
getCompletions(
|
||||
document: Document,
|
||||
position: Position,
|
||||
completionContext?: CompletionContext
|
||||
): CompletionList | null {
|
||||
const triggerCharacter = completionContext?.triggerCharacter;
|
||||
const triggerKind = completionContext?.triggerKind;
|
||||
const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter;
|
||||
|
||||
if (
|
||||
isCustomTriggerCharacter &&
|
||||
triggerCharacter &&
|
||||
!this.triggerCharacters.has(triggerCharacter)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(this.isInsideFrontmatter(document, position)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cssDocument = this.getCSSDoc(document);
|
||||
|
||||
if (cssDocument.isInGenerated(position)) {
|
||||
return this.getCompletionsInternal(document, position, cssDocument);
|
||||
}
|
||||
|
||||
const attributeContext = getAttributeContextAtPosition(document, position);
|
||||
if (!attributeContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
|
||||
const [start, end] = attributeContext.valueRange;
|
||||
return this.getCompletionsInternal(
|
||||
document,
|
||||
position,
|
||||
new StyleAttributeDocument(document, start, end)
|
||||
);
|
||||
} else {
|
||||
return getIdClassCompletion(cssDocument, attributeContext);
|
||||
}
|
||||
}
|
||||
|
||||
private getCompletionsInternal(
|
||||
document: Document,
|
||||
position: Position,
|
||||
cssDocument: CSSDocumentBase
|
||||
) {
|
||||
if (isSASS(cssDocument)) {
|
||||
// the css language service does not support sass, still we can use
|
||||
// the emmet helper directly to at least get emmet completions
|
||||
return doEmmetComplete(document, position, 'sass', this.configManager.getEmmetConfig());
|
||||
}
|
||||
|
||||
const type = extractLanguage(cssDocument);
|
||||
|
||||
const lang = getLanguageService(type);
|
||||
const emmetResults: CompletionList = {
|
||||
isIncomplete: true,
|
||||
items: []
|
||||
};
|
||||
if (false /* this.configManager.getConfig().css.completions.emmet */) {
|
||||
lang.setCompletionParticipants([
|
||||
getEmmetCompletionParticipants(
|
||||
cssDocument,
|
||||
cssDocument.getGeneratedPosition(position),
|
||||
getLanguage(type),
|
||||
this.configManager.getEmmetConfig(),
|
||||
emmetResults
|
||||
)
|
||||
]);
|
||||
}
|
||||
const results = lang.doComplete(
|
||||
cssDocument,
|
||||
cssDocument.getGeneratedPosition(position),
|
||||
cssDocument.stylesheet
|
||||
);
|
||||
return CompletionList.create(
|
||||
[...(results ? results.items : []), ...emmetResults.items].map((completionItem) =>
|
||||
mapCompletionItemToOriginal(cssDocument, completionItem)
|
||||
),
|
||||
// Emmet completions change on every keystroke, so they are never complete
|
||||
emmetResults.items.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
private inStyleAttributeWithoutInterpolation(
|
||||
attrContext: AttributeContext,
|
||||
text: string
|
||||
): attrContext is Required<AttributeContext> {
|
||||
return (
|
||||
attrContext.name === 'style' &&
|
||||
!!attrContext.valueRange &&
|
||||
!text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{')
|
||||
);
|
||||
}
|
||||
|
||||
private getCSSDoc(document: Document) {
|
||||
let cssDoc = this.documents.get(document);
|
||||
if (!cssDoc || cssDoc.version < document.version) {
|
||||
cssDoc = new CSSDocument(document);
|
||||
this.documents.set(document, cssDoc);
|
||||
}
|
||||
return cssDoc;
|
||||
}
|
||||
|
||||
private isInsideFrontmatter(document: Document, position: Position) {
|
||||
return isInsideFrontmatter(document.getText(), document.offsetAt(position));
|
||||
}
|
||||
}
|
||||
|
||||
function isSASS(document: CSSDocumentBase) {
|
||||
switch (extractLanguage(document)) {
|
||||
case 'sass':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractLanguage(document: CSSDocumentBase): string {
|
||||
const lang = document.languageId;
|
||||
return lang.replace(/^text\//, '');
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { Stylesheet } from 'vscode-css-languageservice';
|
||||
import { Position } from 'vscode-languageserver';
|
||||
import { getLanguageService } from './service';
|
||||
import { Document, DocumentMapper, ReadableDocument } from '../../core/documents';
|
||||
|
||||
const PREFIX = '__ {';
|
||||
const SUFFIX = '}';
|
||||
|
||||
export class StyleAttributeDocument extends ReadableDocument implements DocumentMapper {
|
||||
readonly version = this.parent.version;
|
||||
|
||||
public stylesheet: Stylesheet;
|
||||
public languageId = 'css';
|
||||
|
||||
constructor(
|
||||
private readonly parent: Document,
|
||||
private readonly attrStart: number,
|
||||
private readonly attrEnd: number
|
||||
) {
|
||||
super();
|
||||
|
||||
this.stylesheet = getLanguageService(this.languageId).parseStylesheet(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fragment position relative to the parent
|
||||
* @param pos Position in fragment
|
||||
*/
|
||||
getOriginalPosition(pos: Position): Position {
|
||||
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: Position): Position {
|
||||
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: Position): boolean {
|
||||
const offset = this.parent.offsetAt(pos);
|
||||
return offset >= this.attrStart && offset <= this.attrEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fragment text from the parent
|
||||
*/
|
||||
getText(): string {
|
||||
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(): number {
|
||||
return PREFIX.length + this.attrEnd - this.attrStart + SUFFIX.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the parent file path
|
||||
*/
|
||||
getFilePath(): string | null {
|
||||
return this.parent.getFilePath();
|
||||
}
|
||||
|
||||
getURL() {
|
||||
return this.parent.getURL();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import { CompletionItem, CompletionItemKind, CompletionList } from 'vscode-languageserver';
|
||||
import { AttributeContext } from '../../../core/documents/parseHtml';
|
||||
import { CSSDocument } from '../CSSDocument';
|
||||
|
||||
export function getIdClassCompletion(
|
||||
cssDoc: CSSDocument,
|
||||
attributeContext: AttributeContext
|
||||
): CompletionList | null {
|
||||
const collectingType = getCollectingType(attributeContext);
|
||||
|
||||
if (!collectingType) {
|
||||
return null;
|
||||
}
|
||||
const items = collectSelectors(cssDoc.stylesheet as CSSNode, collectingType);
|
||||
|
||||
console.log("getIdClassCompletion items", items.length);
|
||||
return CompletionList.create(items);
|
||||
}
|
||||
|
||||
function getCollectingType(attributeContext: AttributeContext): number | undefined {
|
||||
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
|
||||
*/
|
||||
export enum NodeType {
|
||||
ClassSelector = 14,
|
||||
IdentifierSelector = 15
|
||||
}
|
||||
|
||||
export type CSSNode = {
|
||||
type: number;
|
||||
children: CSSNode[] | undefined;
|
||||
getText(): string;
|
||||
};
|
||||
|
||||
export function collectSelectors(stylesheet: CSSNode, type: number) {
|
||||
const result: CSSNode[] = [];
|
||||
walk(stylesheet, (node) => {
|
||||
if (node.type === type) {
|
||||
result.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return result.map(
|
||||
(node): CompletionItem => ({
|
||||
label: node.getText().substring(1),
|
||||
kind: CompletionItemKind.Keyword
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function walk(node: CSSNode, callback: (node: CSSNode) => void) {
|
||||
callback(node);
|
||||
if (node.children) {
|
||||
node.children.forEach((node) => walk(node, callback));
|
||||
}
|
||||
}
|
58
tools/astro-languageserver/src/plugins/css/service.ts
Normal file
58
tools/astro-languageserver/src/plugins/css/service.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
getCSSLanguageService,
|
||||
getSCSSLanguageService,
|
||||
getLESSLanguageService,
|
||||
LanguageService,
|
||||
ICSSDataProvider
|
||||
} from 'vscode-css-languageservice';
|
||||
|
||||
const customDataProvider: ICSSDataProvider = {
|
||||
providePseudoClasses() {
|
||||
return [];
|
||||
},
|
||||
provideProperties() {
|
||||
return [];
|
||||
},
|
||||
provideAtDirectives() {
|
||||
return [];
|
||||
},
|
||||
providePseudoElements() {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const [css, scss, less] = [
|
||||
getCSSLanguageService,
|
||||
getSCSSLanguageService,
|
||||
getLESSLanguageService
|
||||
].map((getService) =>
|
||||
getService({
|
||||
customDataProviders: [customDataProvider]
|
||||
})
|
||||
);
|
||||
|
||||
const langs = {
|
||||
css,
|
||||
scss,
|
||||
less
|
||||
};
|
||||
|
||||
export function getLanguage(kind?: string) {
|
||||
switch (kind) {
|
||||
case 'scss':
|
||||
case 'text/scss':
|
||||
return 'scss' as const;
|
||||
case 'less':
|
||||
case 'text/less':
|
||||
return 'less' as const;
|
||||
case 'css':
|
||||
case 'text/css':
|
||||
default:
|
||||
return 'css' as const;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLanguageService(kind?: string): LanguageService {
|
||||
const lang = getLanguage(kind);
|
||||
return langs[lang];
|
||||
}
|
|
@ -3,3 +3,4 @@ export * from './astro/AstroPlugin';
|
|||
export * from './html/HTMLPlugin';
|
||||
export * from './typescript/TypeScriptPlugin';
|
||||
export * from './interfaces';
|
||||
export * from './css/CSSPlugin';
|
21
yarn.lock
21
yarn.lock
|
@ -10538,9 +10538,9 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0:
|
|||
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
source-map@~0.7.2:
|
||||
source-map@^0.7.3, source-map@~0.7.2:
|
||||
version "0.7.3"
|
||||
resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
|
||||
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
|
||||
|
||||
sourcemap-codec@^1.4.4:
|
||||
|
@ -12075,6 +12075,16 @@ void-elements@^2.0.1:
|
|||
resolved "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz"
|
||||
integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
|
||||
|
||||
vscode-css-languageservice@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.1.tgz#d68a22ea0b34a8356c169cafc7d32564c2ff6e87"
|
||||
integrity sha512-QW0oe/g2y5E2AbVqY7FJNr2Q8uHiAHNSFpptI6xB8Y0KgzVKppOcIVrgmBNzXhFp9IswAwptkdqr8ExSJbqPkQ==
|
||||
dependencies:
|
||||
vscode-languageserver-textdocument "^1.0.1"
|
||||
vscode-languageserver-types "^3.16.0"
|
||||
vscode-nls "^5.0.0"
|
||||
vscode-uri "^3.0.2"
|
||||
|
||||
vscode-emmet-helper@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/vscode-emmet-helper/-/vscode-emmet-helper-2.1.2.tgz"
|
||||
|
@ -12124,7 +12134,7 @@ vscode-languageserver-textdocument@^1.0.1:
|
|||
resolved "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz"
|
||||
integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==
|
||||
|
||||
vscode-languageserver-types@3.16.0, vscode-languageserver-types@^3.15.1:
|
||||
vscode-languageserver-types@3.16.0, vscode-languageserver-types@^3.15.1, vscode-languageserver-types@^3.16.0:
|
||||
version "3.16.0"
|
||||
resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz"
|
||||
integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==
|
||||
|
@ -12151,6 +12161,11 @@ vscode-uri@^2.1.2:
|
|||
resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz"
|
||||
integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==
|
||||
|
||||
vscode-uri@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.2.tgz#ecfd1d066cb8ef4c3a208decdbab9a8c23d055d0"
|
||||
integrity sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA==
|
||||
|
||||
vue@^3.0.10:
|
||||
version "3.0.11"
|
||||
resolved "https://registry.npmjs.org/vue/-/vue-3.0.11.tgz"
|
||||
|
|
Loading…
Reference in a new issue