[ci] yarn format

This commit is contained in:
matthewp 2021-05-17 18:28:30 +00:00 committed by GitHub Actions
parent c3c96bf498
commit 86ed94e0c6
11 changed files with 494 additions and 619 deletions

View file

@ -26,7 +26,7 @@ export class Document implements TextDocument {
this.html = parseHtml(this.content); this.html = parseHtml(this.content);
this.astro = parseAstro(this.content); this.astro = parseAstro(this.content);
this.styleInfo = extractStyleTag(this.content, this.html); this.styleInfo = extractStyleTag(this.content, this.html);
if(this.styleInfo) { if (this.styleInfo) {
this.styleInfo.attributes.lang = 'css'; this.styleInfo.attributes.lang = 'css';
} }
} }

View file

@ -5,140 +5,137 @@ import { Position, TextDocument } from 'vscode-languageserver';
* Represents a textual document. * Represents a textual document.
*/ */
export abstract class ReadableDocument implements TextDocument { export abstract class ReadableDocument implements TextDocument {
/** /**
* Get the text content of the document * Get the text content of the document
*/ */
abstract getText(): string; abstract getText(): string;
/** /**
* Returns the url of the document * Returns the url of the document
*/ */
abstract getURL(): string; abstract getURL(): string;
/** /**
* Returns the file path if the url scheme is file * Returns the file path if the url scheme is file
*/ */
abstract getFilePath(): string | null; abstract getFilePath(): string | null;
/** /**
* Current version of the document. * Current version of the document.
*/ */
public version = 0; public version = 0;
/** /**
* Get the length of the document's content * Get the length of the document's content
*/ */
getTextLength(): number { getTextLength(): number {
return this.getText().length; 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) {
* Get the line and character based on the offset const mid = Math.floor((low + high) / 2);
* @param offset The index of the position if (lineOffsets[mid] > offset) {
*/ high = mid;
positionAt(offset: number): Position { } else {
offset = clamp(offset, 0, this.getTextLength()); low = mid + 1;
}
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]);
} }
/** // low is the least x for which the line offset is larger than the current offset
* Get the index of the line and character position // or array.length if no line offset is larger than the current offset
* @param position Line and character position const line = low - 1;
*/ return Position.create(line, offset - lineOffsets[line]);
offsetAt(position: Position): number { }
const lineOffsets = this.getLineOffsets();
if (position.line >= lineOffsets.length) { /**
return this.getTextLength(); * Get the index of the line and character position
} else if (position.line < 0) { * @param position Line and character position
return 0; */
} offsetAt(position: Position): number {
const lineOffsets = this.getLineOffsets();
const lineOffset = lineOffsets[position.line]; if (position.line >= lineOffsets.length) {
const nextLineOffset = return this.getTextLength();
position.line + 1 < lineOffsets.length } else if (position.line < 0) {
? lineOffsets[position.line + 1] return 0;
: this.getTextLength();
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
} }
private getLineOffsets() { const lineOffset = lineOffsets[position.line];
const lineOffsets = []; const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : this.getTextLength();
const text = this.getText();
let isLineStart = true;
for (let i = 0; i < text.length; i++) { return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
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) { private getLineOffsets() {
lineOffsets.push(text.length); const lineOffsets = [];
} const text = this.getText();
let isLineStart = true;
return lineOffsets; 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) {
* Implements TextDocument lineOffsets.push(text.length);
*/
get uri(): string {
return this.getURL();
} }
get lineCount(): number { return lineOffsets;
return this.getText().split(/\r?\n/).length; }
}
abstract languageId: string; /**
* 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. * Represents a textual document that can be manipulated.
*/ */
export abstract class WritableDocument extends ReadableDocument { export abstract class WritableDocument extends ReadableDocument {
/** /**
* Set the text content of the document * Set the text content of the document
* @param text The new text content * @param text The new text content
*/ */
abstract setText(text: string): void; abstract setText(text: string): void;
/** /**
* Update the text between two positions. * Update the text between two positions.
* @param text The new text slice * @param text The new text slice
* @param start Start offset of the new text * @param start Start offset of the new text
* @param end End offset of the new text * @param end End offset of the new text
*/ */
update(text: string, start: number, end: number): void { update(text: string, start: number, end: number): void {
const content = this.getText(); const content = this.getText();
this.setText(content.slice(0, start) + text + content.slice(end)); this.setText(content.slice(0, start) + text + content.slice(end));
} }
} }

View file

@ -11,7 +11,7 @@ import {
CodeAction, CodeAction,
SelectionRange, SelectionRange,
TextEdit, TextEdit,
InsertReplaceEdit InsertReplaceEdit,
} from 'vscode-languageserver'; } from 'vscode-languageserver';
import { TagInformation, offsetAt, positionAt } from './utils'; import { TagInformation, offsetAt, positionAt } from './utils';
import { SourceMapConsumer } from 'source-map'; import { SourceMapConsumer } from 'source-map';
@ -47,331 +47,271 @@ export interface DocumentMapper {
} }
/** /**
* Does not map, returns positions as is. * Does not map, returns positions as is.
*/ */
export class IdentityMapper implements DocumentMapper { export class IdentityMapper implements DocumentMapper {
constructor(private url: string, private parent?: DocumentMapper) {} constructor(private url: string, private parent?: DocumentMapper) {}
getOriginalPosition(generatedPosition: Position): Position { getOriginalPosition(generatedPosition: Position): Position {
if (this.parent) { if (this.parent) {
generatedPosition = this.getOriginalPosition(generatedPosition); generatedPosition = this.getOriginalPosition(generatedPosition);
} }
return generatedPosition; return generatedPosition;
} }
getGeneratedPosition(originalPosition: Position): Position { getGeneratedPosition(originalPosition: Position): Position {
if (this.parent) { if (this.parent) {
originalPosition = this.getGeneratedPosition(originalPosition); originalPosition = this.getGeneratedPosition(originalPosition);
} }
return originalPosition; return originalPosition;
} }
isInGenerated(position: Position): boolean { isInGenerated(position: Position): boolean {
if (this.parent && !this.parent.isInGenerated(position)) { if (this.parent && !this.parent.isInGenerated(position)) {
return false; return false;
} }
return true; return true;
} }
getURL(): string { getURL(): string {
return this.url; return this.url;
} }
destroy() { destroy() {
this.parent?.destroy?.(); this.parent?.destroy?.();
} }
} }
/** /**
* Maps positions in a fragment relative to a parent. * Maps positions in a fragment relative to a parent.
*/ */
export class FragmentMapper implements DocumentMapper { export class FragmentMapper implements DocumentMapper {
constructor( constructor(private originalText: string, private tagInfo: TagInformation, private url: string) {}
private originalText: string,
private tagInfo: TagInformation,
private url: string
) {}
getOriginalPosition(generatedPosition: Position): Position { getOriginalPosition(generatedPosition: Position): Position {
const parentOffset = this.offsetInParent(offsetAt(generatedPosition, this.tagInfo.content)); const parentOffset = this.offsetInParent(offsetAt(generatedPosition, this.tagInfo.content));
return positionAt(parentOffset, this.originalText); return positionAt(parentOffset, this.originalText);
} }
private offsetInParent(offset: number): number { private offsetInParent(offset: number): number {
return this.tagInfo.start + offset; return this.tagInfo.start + offset;
} }
getGeneratedPosition(originalPosition: Position): Position { getGeneratedPosition(originalPosition: Position): Position {
const fragmentOffset = offsetAt(originalPosition, this.originalText) - this.tagInfo.start; const fragmentOffset = offsetAt(originalPosition, this.originalText) - this.tagInfo.start;
return positionAt(fragmentOffset, this.tagInfo.content); return positionAt(fragmentOffset, this.tagInfo.content);
} }
isInGenerated(pos: Position): boolean { isInGenerated(pos: Position): boolean {
const offset = offsetAt(pos, this.originalText); const offset = offsetAt(pos, this.originalText);
return offset >= this.tagInfo.start && offset <= this.tagInfo.end; return offset >= this.tagInfo.start && offset <= this.tagInfo.end;
} }
getURL(): string { getURL(): string {
return this.url; return this.url;
} }
} }
export class SourceMapDocumentMapper implements DocumentMapper { export class SourceMapDocumentMapper implements DocumentMapper {
constructor( constructor(protected consumer: SourceMapConsumer, protected sourceUri: string, private parent?: DocumentMapper) {}
protected consumer: SourceMapConsumer,
protected sourceUri: string,
private parent?: DocumentMapper
) {}
getOriginalPosition(generatedPosition: Position): Position { getOriginalPosition(generatedPosition: Position): Position {
if (this.parent) { if (this.parent) {
generatedPosition = this.parent.getOriginalPosition(generatedPosition); generatedPosition = this.parent.getOriginalPosition(generatedPosition);
} }
if (generatedPosition.line < 0) { if (generatedPosition.line < 0) {
return { line: -1, character: -1 }; return { line: -1, character: -1 };
} }
const mapped = this.consumer.originalPositionFor({ const mapped = this.consumer.originalPositionFor({
line: generatedPosition.line + 1, line: generatedPosition.line + 1,
column: generatedPosition.character column: generatedPosition.character,
}); });
if (!mapped) { if (!mapped) {
return { line: -1, character: -1 }; return { line: -1, character: -1 };
} }
if (mapped.line === 0) { if (mapped.line === 0) {
console.log('Got 0 mapped line from', generatedPosition, 'col was', mapped.column); console.log('Got 0 mapped line from', generatedPosition, 'col was', mapped.column);
} }
return { return {
line: (mapped.line || 0) - 1, line: (mapped.line || 0) - 1,
character: mapped.column || 0 character: mapped.column || 0,
}; };
} }
getGeneratedPosition(originalPosition: Position): Position { getGeneratedPosition(originalPosition: Position): Position {
if (this.parent) { if (this.parent) {
originalPosition = this.parent.getGeneratedPosition(originalPosition); originalPosition = this.parent.getGeneratedPosition(originalPosition);
} }
const mapped = this.consumer.generatedPositionFor({ const mapped = this.consumer.generatedPositionFor({
line: originalPosition.line + 1, line: originalPosition.line + 1,
column: originalPosition.character, column: originalPosition.character,
source: this.sourceUri source: this.sourceUri,
}); });
if (!mapped) { if (!mapped) {
return { line: -1, character: -1 }; return { line: -1, character: -1 };
} }
const result = { const result = {
line: (mapped.line || 0) - 1, line: (mapped.line || 0) - 1,
character: mapped.column || 0 character: mapped.column || 0,
}; };
if (result.line < 0) {
return result;
}
if (result.line < 0) {
return result; return result;
}
return result;
} }
isInGenerated(position: Position): boolean { isInGenerated(position: Position): boolean {
if (this.parent && !this.isInGenerated(position)) { if (this.parent && !this.isInGenerated(position)) {
return false; return false;
} }
const generated = this.getGeneratedPosition(position); const generated = this.getGeneratedPosition(position);
return generated.line >= 0; return generated.line >= 0;
} }
getURL(): string { getURL(): string {
return this.sourceUri; return this.sourceUri;
} }
/** /**
* Needs to be called when source mapper is no longer needed in order to prevent memory leaks. * Needs to be called when source mapper is no longer needed in order to prevent memory leaks.
*/ */
destroy() { destroy() {
this.parent?.destroy?.(); this.parent?.destroy?.();
this.consumer.destroy(); this.consumer.destroy();
} }
} }
export function mapRangeToOriginal( export function mapRangeToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, range: Range): Range {
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
range: Range
): Range {
// DON'T use Range.create here! Positions might not be mapped // DON'T use Range.create here! Positions might not be mapped
// and therefore return negative numbers, which makes Range.create throw. // and therefore return negative numbers, which makes Range.create throw.
// These invalid position need to be handled // These invalid position need to be handled
// on a case-by-case basis in the calling functions. // on a case-by-case basis in the calling functions.
const originalRange = { const originalRange = {
start: fragment.getOriginalPosition(range.start), start: fragment.getOriginalPosition(range.start),
end: fragment.getOriginalPosition(range.end) end: fragment.getOriginalPosition(range.end),
}; };
// Range may be mapped one character short - reverse that for "in the same line" cases // Range may be mapped one character short - reverse that for "in the same line" cases
if ( if (
originalRange.start.line === originalRange.end.line && originalRange.start.line === originalRange.end.line &&
range.start.line === range.end.line && range.start.line === range.end.line &&
originalRange.end.character - originalRange.start.character === originalRange.end.character - originalRange.start.character === range.end.character - range.start.character - 1
range.end.character - range.start.character - 1
) { ) {
originalRange.end.character += 1; originalRange.end.character += 1;
} }
return originalRange; return originalRange;
} }
export function mapRangeToGenerated(fragment: DocumentMapper, range: Range): Range { export function mapRangeToGenerated(fragment: DocumentMapper, range: Range): Range {
return Range.create( return Range.create(fragment.getGeneratedPosition(range.start), fragment.getGeneratedPosition(range.end));
fragment.getGeneratedPosition(range.start),
fragment.getGeneratedPosition(range.end)
);
} }
export function mapCompletionItemToOriginal( export function mapCompletionItemToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, item: CompletionItem): CompletionItem {
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
item: CompletionItem
): CompletionItem {
if (!item.textEdit) { if (!item.textEdit) {
return item; return item;
} }
return { return {
...item, ...item,
textEdit: mapEditToOriginal(fragment, item.textEdit) textEdit: mapEditToOriginal(fragment, item.textEdit),
}; };
} }
export function mapHoverToParent( export function mapHoverToParent(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, hover: Hover): Hover {
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
hover: Hover
): Hover {
if (!hover.range) { if (!hover.range) {
return hover; return hover;
} }
return { ...hover, range: mapRangeToOriginal(fragment, hover.range) }; return { ...hover, range: mapRangeToOriginal(fragment, hover.range) };
} }
export function mapObjWithRangeToOriginal<T extends { range: Range }>( export function mapObjWithRangeToOriginal<T extends { range: Range }>(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, objWithRange: T): T {
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
objWithRange: T
): T {
return { ...objWithRange, range: mapRangeToOriginal(fragment, objWithRange.range) }; return { ...objWithRange, range: mapRangeToOriginal(fragment, objWithRange.range) };
} }
export function mapInsertReplaceEditToOriginal( export function mapInsertReplaceEditToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, edit: InsertReplaceEdit): InsertReplaceEdit {
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
edit: InsertReplaceEdit
): InsertReplaceEdit {
return { return {
...edit, ...edit,
insert: mapRangeToOriginal(fragment, edit.insert), insert: mapRangeToOriginal(fragment, edit.insert),
replace: mapRangeToOriginal(fragment, edit.replace) replace: mapRangeToOriginal(fragment, edit.replace),
}; };
} }
export function mapEditToOriginal( export function mapEditToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, edit: TextEdit | InsertReplaceEdit): TextEdit | InsertReplaceEdit {
fragment: Pick<DocumentMapper, 'getOriginalPosition'>, return TextEdit.is(edit) ? mapObjWithRangeToOriginal(fragment, edit) : mapInsertReplaceEditToOriginal(fragment, edit);
edit: TextEdit | InsertReplaceEdit
): TextEdit | InsertReplaceEdit {
return TextEdit.is(edit)
? mapObjWithRangeToOriginal(fragment, edit)
: mapInsertReplaceEditToOriginal(fragment, edit);
} }
export function mapDiagnosticToGenerated( export function mapDiagnosticToGenerated(fragment: DocumentMapper, diagnostic: Diagnostic): Diagnostic {
fragment: DocumentMapper,
diagnostic: Diagnostic
): Diagnostic {
return { ...diagnostic, range: mapRangeToGenerated(fragment, diagnostic.range) }; return { ...diagnostic, range: mapRangeToGenerated(fragment, diagnostic.range) };
} }
export function mapColorPresentationToOriginal( export function mapColorPresentationToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, presentation: ColorPresentation): ColorPresentation {
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
presentation: ColorPresentation
): ColorPresentation {
const item = { const item = {
...presentation ...presentation,
}; };
if (item.textEdit) { if (item.textEdit) {
item.textEdit = mapObjWithRangeToOriginal(fragment, item.textEdit); item.textEdit = mapObjWithRangeToOriginal(fragment, item.textEdit);
} }
if (item.additionalTextEdits) { if (item.additionalTextEdits) {
item.additionalTextEdits = item.additionalTextEdits.map((edit) => item.additionalTextEdits = item.additionalTextEdits.map((edit) => mapObjWithRangeToOriginal(fragment, edit));
mapObjWithRangeToOriginal(fragment, edit)
);
} }
return item; return item;
} }
export function mapSymbolInformationToOriginal( export function mapSymbolInformationToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, info: SymbolInformation): SymbolInformation {
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
info: SymbolInformation
): SymbolInformation {
return { ...info, location: mapObjWithRangeToOriginal(fragment, info.location) }; return { ...info, location: mapObjWithRangeToOriginal(fragment, info.location) };
} }
export function mapLocationLinkToOriginal( export function mapLocationLinkToOriginal(fragment: DocumentMapper, def: LocationLink): LocationLink {
fragment: DocumentMapper,
def: LocationLink
): LocationLink {
return LocationLink.create( return LocationLink.create(
def.targetUri, def.targetUri,
fragment.getURL() === def.targetUri fragment.getURL() === def.targetUri ? mapRangeToOriginal(fragment, def.targetRange) : def.targetRange,
? mapRangeToOriginal(fragment, def.targetRange) fragment.getURL() === def.targetUri ? mapRangeToOriginal(fragment, def.targetSelectionRange) : def.targetSelectionRange,
: def.targetRange, def.originSelectionRange ? mapRangeToOriginal(fragment, def.originSelectionRange) : undefined
fragment.getURL() === def.targetUri
? mapRangeToOriginal(fragment, def.targetSelectionRange)
: def.targetSelectionRange,
def.originSelectionRange
? mapRangeToOriginal(fragment, def.originSelectionRange)
: undefined
); );
} }
export function mapTextDocumentEditToOriginal(fragment: DocumentMapper, edit: TextDocumentEdit) { export function mapTextDocumentEditToOriginal(fragment: DocumentMapper, edit: TextDocumentEdit) {
if (edit.textDocument.uri !== fragment.getURL()) { if (edit.textDocument.uri !== fragment.getURL()) {
return edit; return edit;
} }
return TextDocumentEdit.create( return TextDocumentEdit.create(
edit.textDocument, edit.textDocument,
edit.edits.map((textEdit) => mapObjWithRangeToOriginal(fragment, textEdit)) edit.edits.map((textEdit) => mapObjWithRangeToOriginal(fragment, textEdit))
); );
} }
export function mapCodeActionToOriginal(fragment: DocumentMapper, codeAction: CodeAction) { export function mapCodeActionToOriginal(fragment: DocumentMapper, codeAction: CodeAction) {
return CodeAction.create( return CodeAction.create(
codeAction.title, codeAction.title,
{ {
documentChanges: codeAction.edit!.documentChanges!.map((edit) => documentChanges: codeAction.edit!.documentChanges!.map((edit) => mapTextDocumentEditToOriginal(fragment, edit as TextDocumentEdit)),
mapTextDocumentEditToOriginal(fragment, edit as TextDocumentEdit) },
) codeAction.kind
},
codeAction.kind
); );
} }
export function mapSelectionRangeToParent( export function mapSelectionRangeToParent(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, selectionRange: SelectionRange): SelectionRange {
fragment: Pick<DocumentMapper, 'getOriginalPosition'>,
selectionRange: SelectionRange
): SelectionRange {
const { range, parent } = selectionRange; const { range, parent } = selectionRange;
return SelectionRange.create( return SelectionRange.create(mapRangeToOriginal(fragment, range), parent && mapSelectionRangeToParent(fragment, parent));
mapRangeToOriginal(fragment, range), }
parent && mapSelectionRangeToParent(fragment, parent)
);
}

View file

@ -2,4 +2,4 @@ export * from './Document';
export * from './DocumentBase'; export * from './DocumentBase';
export * from './DocumentManager'; export * from './DocumentManager';
export * from './DocumentMapper'; export * from './DocumentMapper';
export * from './utils'; export * from './utils';

View file

@ -1,6 +1,6 @@
import { HTMLDocument, Node, Position } from 'vscode-html-languageservice'; import { HTMLDocument, Node, Position } from 'vscode-html-languageservice';
import { clamp } from '../../utils'; import { clamp } from '../../utils';
import {parseHtml} from './parseHtml'; import { parseHtml } from './parseHtml';
export interface TagInformation { export interface TagInformation {
content: string; content: string;
@ -12,28 +12,23 @@ export interface TagInformation {
container: { start: number; end: number }; container: { start: number; end: number };
} }
function parseAttributes( function parseAttributes(rawAttrs: Record<string, string | null> | undefined): Record<string, string> {
rawAttrs: Record<string, string | null> | undefined
): Record<string, string> {
const attrs: Record<string, string> = {}; const attrs: Record<string, string> = {};
if (!rawAttrs) { if (!rawAttrs) {
return attrs; return attrs;
} }
Object.keys(rawAttrs).forEach((attrName) => { Object.keys(rawAttrs).forEach((attrName) => {
const attrValue = rawAttrs[attrName]; const attrValue = rawAttrs[attrName];
attrs[attrName] = attrValue === null ? attrName : removeOuterQuotes(attrValue); attrs[attrName] = attrValue === null ? attrName : removeOuterQuotes(attrValue);
}); });
return attrs; return attrs;
function removeOuterQuotes(attrValue: string) { function removeOuterQuotes(attrValue: string) {
if ( if ((attrValue.startsWith('"') && attrValue.endsWith('"')) || (attrValue.startsWith("'") && attrValue.endsWith("'"))) {
(attrValue.startsWith('"') && attrValue.endsWith('"')) || return attrValue.slice(1, attrValue.length - 1);
(attrValue.startsWith("'") && attrValue.endsWith("'")) }
) { return attrValue;
return attrValue.slice(1, attrValue.length - 1);
}
return attrValue;
} }
} }
@ -163,8 +158,8 @@ function getLineOffsets(text: string) {
} }
export function* walk(node: Node): Generator<Node, void, unknown> { export function* walk(node: Node): Generator<Node, void, unknown> {
for(let child of node.children) { for (let child of node.children) {
yield * walk(child); yield* walk(child);
} }
yield node; yield node;
} }
@ -202,18 +197,13 @@ export function* walk(node: Node, startIndex = 0) {
* @param source text content to extract tag from * @param source text content to extract tag from
* @param tag the tag to extract * @param tag the tag to extract
*/ */
function extractTags( function extractTags(text: string, tag: 'script' | 'style' | 'template', html?: HTMLDocument): TagInformation[] {
text: string,
tag: 'script' | 'style' | 'template',
html?: HTMLDocument
): TagInformation[] {
const rootNodes = html?.roots || parseHtml(text).roots; const rootNodes = html?.roots || parseHtml(text).roots;
const matchedNodes = rootNodes const matchedNodes = rootNodes.filter((node) => node.tag === tag);
.filter((node) => node.tag === tag);
if(tag === 'style' && !matchedNodes.length && rootNodes.length && rootNodes[0].tag === 'html') { if (tag === 'style' && !matchedNodes.length && rootNodes.length && rootNodes[0].tag === 'html') {
for(let child of walk(rootNodes[0])) { for (let child of walk(rootNodes[0])) {
if(child.tag === 'style') { if (child.tag === 'style') {
matchedNodes.push(child); matchedNodes.push(child);
} }
} }
@ -222,34 +212,34 @@ export function* walk(node: Node, startIndex = 0) {
return matchedNodes.map(transformToTagInfo); return matchedNodes.map(transformToTagInfo);
function transformToTagInfo(matchedNode: Node) { function transformToTagInfo(matchedNode: Node) {
const start = matchedNode.startTagEnd ?? matchedNode.start; const start = matchedNode.startTagEnd ?? matchedNode.start;
const end = matchedNode.endTagStart ?? matchedNode.end; const end = matchedNode.endTagStart ?? matchedNode.end;
const startPos = positionAt(start, text); const startPos = positionAt(start, text);
const endPos = positionAt(end, text); const endPos = positionAt(end, text);
const container = { const container = {
start: matchedNode.start, start: matchedNode.start,
end: matchedNode.end end: matchedNode.end,
}; };
const content = text.substring(start, end); const content = text.substring(start, end);
return { return {
content, content,
attributes: parseAttributes(matchedNode.attributes), attributes: parseAttributes(matchedNode.attributes),
start, start,
end, end,
startPos, startPos,
endPos, endPos,
container container,
}; };
} }
} }
export function extractStyleTag(source: string, html?: HTMLDocument): TagInformation | null { export function extractStyleTag(source: string, html?: HTMLDocument): TagInformation | null {
const styles = extractTags(source, 'style', html); const styles = extractTags(source, 'style', html);
if (!styles.length) { if (!styles.length) {
return null; return null;
} }
// There can only be one style tag // There can only be one style tag
return styles[0]; return styles[0];
} }

View file

@ -4,92 +4,92 @@ import { getLanguageService } from './service';
import { Document, DocumentMapper, ReadableDocument, TagInformation } from '../../core/documents/index'; import { Document, DocumentMapper, ReadableDocument, TagInformation } from '../../core/documents/index';
export interface CSSDocumentBase extends DocumentMapper, TextDocument { export interface CSSDocumentBase extends DocumentMapper, TextDocument {
languageId: string; languageId: string;
stylesheet: Stylesheet; stylesheet: Stylesheet;
} }
export class CSSDocument extends ReadableDocument implements DocumentMapper { export class CSSDocument extends ReadableDocument implements DocumentMapper {
private styleInfo: Pick<TagInformation, 'attributes' | 'start' | 'end'>; private styleInfo: Pick<TagInformation, 'attributes' | 'start' | 'end'>;
readonly version = this.parent.version; readonly version = this.parent.version;
public stylesheet: Stylesheet; public stylesheet: Stylesheet;
public languageId: string; public languageId: string;
constructor(private parent: Document) { constructor(private parent: Document) {
super(); super();
if (this.parent.styleInfo) { if (this.parent.styleInfo) {
this.styleInfo = this.parent.styleInfo; this.styleInfo = this.parent.styleInfo;
} else { } else {
this.styleInfo = { this.styleInfo = {
attributes: {}, attributes: {},
start: -1, start: -1,
end: -1 end: -1,
}; };
}
this.languageId = this.language;
this.stylesheet = getLanguageService(this.language).parseStylesheet(this);
} }
/** this.languageId = this.language;
* Get the fragment position relative to the parent this.stylesheet = getLanguageService(this.language).parseStylesheet(this);
* @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 * Get the fragment position relative to the parent
* @param pos Position in parent * @param pos Position in fragment
*/ */
getGeneratedPosition(pos: Position): Position { getOriginalPosition(pos: Position): Position {
const fragmentOffset = this.parent.offsetAt(pos) - this.styleInfo.start; const parentOffset = this.styleInfo.start + this.offsetAt(pos);
return this.positionAt(fragmentOffset); return this.parent.positionAt(parentOffset);
} }
/** /**
* Returns true if the given parent position is inside of this fragment * Get the position relative to the start of the fragment
* @param pos Position in parent * @param pos Position in parent
*/ */
isInGenerated(pos: Position): boolean { getGeneratedPosition(pos: Position): Position {
const offset = this.parent.offsetAt(pos); const fragmentOffset = this.parent.offsetAt(pos) - this.styleInfo.start;
return offset >= this.styleInfo.start && offset <= this.styleInfo.end; return this.positionAt(fragmentOffset);
} }
/** /**
* Get the fragment text from the parent * Returns true if the given parent position is inside of this fragment
*/ * @param pos Position in parent
getText(): string { */
return this.parent.getText().slice(this.styleInfo.start, this.styleInfo.end); isInGenerated(pos: Position): boolean {
} const offset = this.parent.offsetAt(pos);
return offset >= this.styleInfo.start && offset <= this.styleInfo.end;
}
/** /**
* Returns the length of the fragment as calculated from the start and end positon * Get the fragment text from the parent
*/ */
getTextLength(): number { getText(): string {
return this.styleInfo.end - this.styleInfo.start; return this.parent.getText().slice(this.styleInfo.start, this.styleInfo.end);
} }
/** /**
* Return the parent file path * Returns the length of the fragment as calculated from the start and end positon
*/ */
getFilePath(): string | null { getTextLength(): number {
return this.parent.getFilePath(); return this.styleInfo.end - this.styleInfo.start;
} }
getURL() { /**
return this.parent.getURL(); * Return the parent file path
} */
getFilePath(): string | null {
return this.parent.getFilePath();
}
getAttributes() { getURL() {
return this.styleInfo.attributes; return this.parent.getURL();
} }
private get language() { getAttributes() {
const attrs = this.getAttributes(); return this.styleInfo.attributes;
return attrs.lang || attrs.type || 'css'; }
}
private get language() {
const attrs = this.getAttributes();
return attrs.lang || attrs.type || 'css';
}
} }

View file

@ -26,24 +26,16 @@ export class CSSPlugin implements CompletionsProvider {
}); });
} }
getCompletions( getCompletions(document: Document, position: Position, completionContext?: CompletionContext): CompletionList | null {
document: Document,
position: Position,
completionContext?: CompletionContext
): CompletionList | null {
const triggerCharacter = completionContext?.triggerCharacter; const triggerCharacter = completionContext?.triggerCharacter;
const triggerKind = completionContext?.triggerKind; const triggerKind = completionContext?.triggerKind;
const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter; const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter;
if ( if (isCustomTriggerCharacter && triggerCharacter && !this.triggerCharacters.has(triggerCharacter)) {
isCustomTriggerCharacter && return null;
triggerCharacter &&
!this.triggerCharacters.has(triggerCharacter)
) {
return null;
} }
if(this.isInsideFrontmatter(document, position)) { if (this.isInsideFrontmatter(document, position)) {
return null; return null;
} }
@ -55,82 +47,55 @@ export class CSSPlugin implements CompletionsProvider {
const attributeContext = getAttributeContextAtPosition(document, position); const attributeContext = getAttributeContextAtPosition(document, position);
if (!attributeContext) { if (!attributeContext) {
return null; return null;
} }
if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) { if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
const [start, end] = attributeContext.valueRange; const [start, end] = attributeContext.valueRange;
return this.getCompletionsInternal( return this.getCompletionsInternal(document, position, new StyleAttributeDocument(document, start, end));
document,
position,
new StyleAttributeDocument(document, start, end)
);
} else { } else {
return getIdClassCompletion(cssDocument, attributeContext); return getIdClassCompletion(cssDocument, attributeContext);
} }
} }
private getCompletionsInternal( private getCompletionsInternal(document: Document, position: Position, cssDocument: CSSDocumentBase) {
document: Document, if (isSASS(cssDocument)) {
position: Position, // the css language service does not support sass, still we can use
cssDocument: CSSDocumentBase // the emmet helper directly to at least get emmet completions
) { return doEmmetComplete(document, position, 'sass', this.configManager.getEmmetConfig());
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 type = extractLanguage(cssDocument);
const lang = getLanguageService(type); const lang = getLanguageService(type);
const emmetResults: CompletionList = { const emmetResults: CompletionList = {
isIncomplete: true, isIncomplete: true,
items: [] items: [],
}; };
if (false /* this.configManager.getConfig().css.completions.emmet */) { if (false /* this.configManager.getConfig().css.completions.emmet */) {
lang.setCompletionParticipants([ lang.setCompletionParticipants([
getEmmetCompletionParticipants( getEmmetCompletionParticipants(cssDocument, cssDocument.getGeneratedPosition(position), getLanguage(type), this.configManager.getEmmetConfig(), emmetResults),
cssDocument, ]);
cssDocument.getGeneratedPosition(position), }
getLanguage(type), const results = lang.doComplete(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
this.configManager.getEmmetConfig(), return CompletionList.create(
emmetResults [...(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
}
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 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) { private getCSSDoc(document: Document) {
let cssDoc = this.documents.get(document); let cssDoc = this.documents.get(document);
if (!cssDoc || cssDoc.version < document.version) { if (!cssDoc || cssDoc.version < document.version) {
cssDoc = new CSSDocument(document); cssDoc = new CSSDocument(document);
this.documents.set(document, cssDoc); this.documents.set(document, cssDoc);
} }
return cssDoc; return cssDoc;
} }
private isInsideFrontmatter(document: Document, position: Position) { private isInsideFrontmatter(document: Document, position: Position) {
@ -140,14 +105,14 @@ export class CSSPlugin implements CompletionsProvider {
function isSASS(document: CSSDocumentBase) { function isSASS(document: CSSDocumentBase) {
switch (extractLanguage(document)) { switch (extractLanguage(document)) {
case 'sass': case 'sass':
return true; return true;
default: default:
return false; return false;
} }
} }
function extractLanguage(document: CSSDocumentBase): string { function extractLanguage(document: CSSDocumentBase): string {
const lang = document.languageId; const lang = document.languageId;
return lang.replace(/^text\//, ''); return lang.replace(/^text\//, '');
} }

View file

@ -7,70 +7,66 @@ const PREFIX = '__ {';
const SUFFIX = '}'; const SUFFIX = '}';
export class StyleAttributeDocument extends ReadableDocument implements DocumentMapper { export class StyleAttributeDocument extends ReadableDocument implements DocumentMapper {
readonly version = this.parent.version; readonly version = this.parent.version;
public stylesheet: Stylesheet; public stylesheet: Stylesheet;
public languageId = 'css'; public languageId = 'css';
constructor( constructor(private readonly parent: Document, private readonly attrStart: number, private readonly attrEnd: number) {
private readonly parent: Document, super();
private readonly attrStart: number,
private readonly attrEnd: number
) {
super();
this.stylesheet = getLanguageService(this.languageId).parseStylesheet(this); this.stylesheet = getLanguageService(this.languageId).parseStylesheet(this);
} }
/** /**
* Get the fragment position relative to the parent * Get the fragment position relative to the parent
* @param pos Position in fragment * @param pos Position in fragment
*/ */
getOriginalPosition(pos: Position): Position { getOriginalPosition(pos: Position): Position {
const parentOffset = this.attrStart + this.offsetAt(pos) - PREFIX.length; const parentOffset = this.attrStart + this.offsetAt(pos) - PREFIX.length;
return this.parent.positionAt(parentOffset); return this.parent.positionAt(parentOffset);
} }
/** /**
* Get the position relative to the start of the fragment * Get the position relative to the start of the fragment
* @param pos Position in parent * @param pos Position in parent
*/ */
getGeneratedPosition(pos: Position): Position { getGeneratedPosition(pos: Position): Position {
const fragmentOffset = this.parent.offsetAt(pos) - this.attrStart + PREFIX.length; const fragmentOffset = this.parent.offsetAt(pos) - this.attrStart + PREFIX.length;
return this.positionAt(fragmentOffset); return this.positionAt(fragmentOffset);
} }
/** /**
* Returns true if the given parent position is inside of this fragment * Returns true if the given parent position is inside of this fragment
* @param pos Position in parent * @param pos Position in parent
*/ */
isInGenerated(pos: Position): boolean { isInGenerated(pos: Position): boolean {
const offset = this.parent.offsetAt(pos); const offset = this.parent.offsetAt(pos);
return offset >= this.attrStart && offset <= this.attrEnd; return offset >= this.attrStart && offset <= this.attrEnd;
} }
/** /**
* Get the fragment text from the parent * Get the fragment text from the parent
*/ */
getText(): string { getText(): string {
return PREFIX + this.parent.getText().slice(this.attrStart, this.attrEnd) + SUFFIX; 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 * Returns the length of the fragment as calculated from the start and end position
*/ */
getTextLength(): number { getTextLength(): number {
return PREFIX.length + this.attrEnd - this.attrStart + SUFFIX.length; return PREFIX.length + this.attrEnd - this.attrStart + SUFFIX.length;
} }
/** /**
* Return the parent file path * Return the parent file path
*/ */
getFilePath(): string | null { getFilePath(): string | null {
return this.parent.getFilePath(); return this.parent.getFilePath();
} }
getURL() { getURL() {
return this.parent.getURL(); return this.parent.getURL();
} }
} }

View file

@ -2,32 +2,29 @@ import { CompletionItem, CompletionItemKind, CompletionList } from 'vscode-langu
import { AttributeContext } from '../../../core/documents/parseHtml'; import { AttributeContext } from '../../../core/documents/parseHtml';
import { CSSDocument } from '../CSSDocument'; import { CSSDocument } from '../CSSDocument';
export function getIdClassCompletion( export function getIdClassCompletion(cssDoc: CSSDocument, attributeContext: AttributeContext): CompletionList | null {
cssDoc: CSSDocument, const collectingType = getCollectingType(attributeContext);
attributeContext: AttributeContext
): CompletionList | null {
const collectingType = getCollectingType(attributeContext);
if (!collectingType) { if (!collectingType) {
return null; return null;
} }
const items = collectSelectors(cssDoc.stylesheet as CSSNode, collectingType); const items = collectSelectors(cssDoc.stylesheet as CSSNode, collectingType);
console.log("getIdClassCompletion items", items.length); console.log('getIdClassCompletion items', items.length);
return CompletionList.create(items); return CompletionList.create(items);
} }
function getCollectingType(attributeContext: AttributeContext): number | undefined { function getCollectingType(attributeContext: AttributeContext): number | undefined {
if (attributeContext.inValue) { if (attributeContext.inValue) {
if (attributeContext.name === 'class') { if (attributeContext.name === 'class') {
return NodeType.ClassSelector; return NodeType.ClassSelector;
}
if (attributeContext.name === 'id') {
return NodeType.IdentifierSelector;
}
} else if (attributeContext.name.startsWith('class:')) {
return NodeType.ClassSelector;
} }
if (attributeContext.name === 'id') {
return NodeType.IdentifierSelector;
}
} else if (attributeContext.name.startsWith('class:')) {
return NodeType.ClassSelector;
}
} }
/** /**
@ -36,35 +33,35 @@ function getCollectingType(attributeContext: AttributeContext): number | undefin
* The enum is not exported. we have to update this whenever it changes * The enum is not exported. we have to update this whenever it changes
*/ */
export enum NodeType { export enum NodeType {
ClassSelector = 14, ClassSelector = 14,
IdentifierSelector = 15 IdentifierSelector = 15,
} }
export type CSSNode = { export type CSSNode = {
type: number; type: number;
children: CSSNode[] | undefined; children: CSSNode[] | undefined;
getText(): string; getText(): string;
}; };
export function collectSelectors(stylesheet: CSSNode, type: number) { export function collectSelectors(stylesheet: CSSNode, type: number) {
const result: CSSNode[] = []; const result: CSSNode[] = [];
walk(stylesheet, (node) => { walk(stylesheet, (node) => {
if (node.type === type) { if (node.type === type) {
result.push(node); result.push(node);
} }
}); });
return result.map( return result.map(
(node): CompletionItem => ({ (node): CompletionItem => ({
label: node.getText().substring(1), label: node.getText().substring(1),
kind: CompletionItemKind.Keyword kind: CompletionItemKind.Keyword,
}) })
); );
} }
function walk(node: CSSNode, callback: (node: CSSNode) => void) { function walk(node: CSSNode, callback: (node: CSSNode) => void) {
callback(node); callback(node);
if (node.children) { if (node.children) {
node.children.forEach((node) => walk(node, callback)); node.children.forEach((node) => walk(node, callback));
} }
} }

View file

@ -1,58 +1,48 @@
import { import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageService, ICSSDataProvider } from 'vscode-css-languageservice';
getCSSLanguageService,
getSCSSLanguageService,
getLESSLanguageService,
LanguageService,
ICSSDataProvider
} from 'vscode-css-languageservice';
const customDataProvider: ICSSDataProvider = { const customDataProvider: ICSSDataProvider = {
providePseudoClasses() { providePseudoClasses() {
return []; return [];
}, },
provideProperties() { provideProperties() {
return []; return [];
}, },
provideAtDirectives() { provideAtDirectives() {
return []; return [];
}, },
providePseudoElements() { providePseudoElements() {
return []; return [];
} },
}; };
const [css, scss, less] = [ const [css, scss, less] = [getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService].map((getService) =>
getCSSLanguageService,
getSCSSLanguageService,
getLESSLanguageService
].map((getService) =>
getService({ getService({
customDataProviders: [customDataProvider] customDataProviders: [customDataProvider],
}) })
); );
const langs = { const langs = {
css, css,
scss, scss,
less less,
}; };
export function getLanguage(kind?: string) { export function getLanguage(kind?: string) {
switch (kind) { switch (kind) {
case 'scss': case 'scss':
case 'text/scss': case 'text/scss':
return 'scss' as const; return 'scss' as const;
case 'less': case 'less':
case 'text/less': case 'text/less':
return 'less' as const; return 'less' as const;
case 'css': case 'css':
case 'text/css': case 'text/css':
default: default:
return 'css' as const; return 'css' as const;
} }
} }
export function getLanguageService(kind?: string): LanguageService { export function getLanguageService(kind?: string): LanguageService {
const lang = getLanguage(kind); const lang = getLanguage(kind);
return langs[lang]; return langs[lang];
} }

View file

@ -3,4 +3,4 @@ export * from './astro/AstroPlugin';
export * from './html/HTMLPlugin'; export * from './html/HTMLPlugin';
export * from './typescript/TypeScriptPlugin'; export * from './typescript/TypeScriptPlugin';
export * from './interfaces'; export * from './interfaces';
export * from './css/CSSPlugin'; export * from './css/CSSPlugin';