[ci] yarn format
This commit is contained in:
parent
c3c96bf498
commit
86ed94e0c6
11 changed files with 494 additions and 619 deletions
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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\//, '');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in a new issue