diff --git a/tools/astro-languageserver/src/core/documents/Document.ts b/tools/astro-languageserver/src/core/documents/Document.ts index 5ee080fdf..04a460a08 100644 --- a/tools/astro-languageserver/src/core/documents/Document.ts +++ b/tools/astro-languageserver/src/core/documents/Document.ts @@ -26,7 +26,7 @@ export class Document implements TextDocument { this.html = parseHtml(this.content); this.astro = parseAstro(this.content); this.styleInfo = extractStyleTag(this.content, this.html); - if(this.styleInfo) { + if (this.styleInfo) { this.styleInfo.attributes.lang = 'css'; } } diff --git a/tools/astro-languageserver/src/core/documents/DocumentBase.ts b/tools/astro-languageserver/src/core/documents/DocumentBase.ts index 19120f1c0..299feeb62 100644 --- a/tools/astro-languageserver/src/core/documents/DocumentBase.ts +++ b/tools/astro-languageserver/src/core/documents/DocumentBase.ts @@ -5,140 +5,137 @@ import { Position, TextDocument } from 'vscode-languageserver'; * Represents a textual document. */ export abstract class ReadableDocument implements TextDocument { - /** - * Get the text content of the document - */ - abstract getText(): string; + /** + * Get the text content of the document + */ + abstract getText(): string; - /** - * Returns the url of the document - */ - abstract getURL(): string; + /** + * Returns the url of the document + */ + abstract getURL(): string; - /** - * Returns the file path if the url scheme is file - */ - abstract getFilePath(): string | null; + /** + * Returns the file path if the url scheme is file + */ + abstract getFilePath(): string | null; - /** - * Current version of the document. - */ - public version = 0; + /** + * Current version of the document. + */ + public version = 0; - /** - * Get the length of the document's content - */ - getTextLength(): number { - return this.getText().length; + /** + * Get the length of the document's content + */ + getTextLength(): number { + return this.getText().length; + } + + /** + * Get the line and character based on the offset + * @param offset The index of the position + */ + positionAt(offset: number): Position { + offset = clamp(offset, 0, this.getTextLength()); + + const lineOffsets = this.getLineOffsets(); + let low = 0; + let high = lineOffsets.length; + if (high === 0) { + return Position.create(0, offset); } - /** - * Get the line and character based on the offset - * @param offset The index of the position - */ - positionAt(offset: number): Position { - offset = clamp(offset, 0, this.getTextLength()); - - const lineOffsets = this.getLineOffsets(); - let low = 0; - let high = lineOffsets.length; - if (high === 0) { - return Position.create(0, offset); - } - - while (low < high) { - const mid = Math.floor((low + high) / 2); - if (lineOffsets[mid] > offset) { - high = mid; - } else { - low = mid + 1; - } - } - - // low is the least x for which the line offset is larger than the current offset - // or array.length if no line offset is larger than the current offset - const line = low - 1; - return Position.create(line, offset - lineOffsets[line]); + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (lineOffsets[mid] > offset) { + high = mid; + } else { + low = mid + 1; + } } - /** - * Get the index of the line and character position - * @param position Line and character position - */ - offsetAt(position: Position): number { - const lineOffsets = this.getLineOffsets(); + // 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]); + } - if (position.line >= lineOffsets.length) { - return this.getTextLength(); - } else if (position.line < 0) { - return 0; - } + /** + * Get the index of the line and character position + * @param position Line and character position + */ + offsetAt(position: Position): number { + const lineOffsets = this.getLineOffsets(); - const lineOffset = lineOffsets[position.line]; - const nextLineOffset = - position.line + 1 < lineOffsets.length - ? lineOffsets[position.line + 1] - : this.getTextLength(); - - return clamp(nextLineOffset, lineOffset, lineOffset + position.character); + if (position.line >= lineOffsets.length) { + return this.getTextLength(); + } else if (position.line < 0) { + return 0; } - private getLineOffsets() { - const lineOffsets = []; - const text = this.getText(); - let isLineStart = true; + const lineOffset = lineOffsets[position.line]; + const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : this.getTextLength(); - 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++; - } - } + return clamp(nextLineOffset, lineOffset, lineOffset + position.character); + } - if (isLineStart && text.length > 0) { - lineOffsets.push(text.length); - } + private getLineOffsets() { + 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++; + } } - /** - * Implements TextDocument - */ - get uri(): string { - return this.getURL(); + if (isLineStart && text.length > 0) { + lineOffsets.push(text.length); } - get lineCount(): number { - return this.getText().split(/\r?\n/).length; - } + return lineOffsets; + } - 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. */ export abstract class WritableDocument extends ReadableDocument { - /** - * Set the text content of the document - * @param text The new text content - */ - abstract setText(text: string): void; + /** + * Set the text content of the document + * @param text The new text content + */ + abstract setText(text: string): void; - /** - * Update the text between two positions. - * @param text The new text slice - * @param start Start offset of the new text - * @param end End offset of the new text - */ - update(text: string, start: number, end: number): void { - const content = this.getText(); - this.setText(content.slice(0, start) + text + content.slice(end)); - } -} \ No newline at end of file + /** + * Update the text between two positions. + * @param text The new text slice + * @param start Start offset of the new text + * @param end End offset of the new text + */ + update(text: string, start: number, end: number): void { + const content = this.getText(); + this.setText(content.slice(0, start) + text + content.slice(end)); + } +} diff --git a/tools/astro-languageserver/src/core/documents/DocumentMapper.ts b/tools/astro-languageserver/src/core/documents/DocumentMapper.ts index 815ce06ff..8a6a6ef29 100644 --- a/tools/astro-languageserver/src/core/documents/DocumentMapper.ts +++ b/tools/astro-languageserver/src/core/documents/DocumentMapper.ts @@ -11,7 +11,7 @@ import { CodeAction, SelectionRange, TextEdit, - InsertReplaceEdit + InsertReplaceEdit, } from 'vscode-languageserver'; import { TagInformation, offsetAt, positionAt } from './utils'; 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 { constructor(private url: string, private parent?: DocumentMapper) {} getOriginalPosition(generatedPosition: Position): Position { - if (this.parent) { - generatedPosition = this.getOriginalPosition(generatedPosition); - } + if (this.parent) { + generatedPosition = this.getOriginalPosition(generatedPosition); + } - return generatedPosition; + return generatedPosition; } getGeneratedPosition(originalPosition: Position): Position { - if (this.parent) { - originalPosition = this.getGeneratedPosition(originalPosition); - } + if (this.parent) { + originalPosition = this.getGeneratedPosition(originalPosition); + } - return originalPosition; + return originalPosition; } isInGenerated(position: Position): boolean { - if (this.parent && !this.parent.isInGenerated(position)) { - return false; - } + if (this.parent && !this.parent.isInGenerated(position)) { + return false; + } - return true; + return true; } getURL(): string { - return this.url; + return this.url; } 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 { - constructor( - private originalText: string, - private tagInfo: TagInformation, - private url: string - ) {} + constructor(private originalText: string, private tagInfo: TagInformation, private url: string) {} getOriginalPosition(generatedPosition: Position): Position { - const parentOffset = this.offsetInParent(offsetAt(generatedPosition, this.tagInfo.content)); - return positionAt(parentOffset, this.originalText); + const parentOffset = this.offsetInParent(offsetAt(generatedPosition, this.tagInfo.content)); + return positionAt(parentOffset, this.originalText); } private offsetInParent(offset: number): number { - return this.tagInfo.start + offset; + return this.tagInfo.start + offset; } getGeneratedPosition(originalPosition: Position): Position { - const fragmentOffset = offsetAt(originalPosition, this.originalText) - this.tagInfo.start; - return positionAt(fragmentOffset, this.tagInfo.content); + const fragmentOffset = offsetAt(originalPosition, this.originalText) - this.tagInfo.start; + return positionAt(fragmentOffset, this.tagInfo.content); } isInGenerated(pos: Position): boolean { - const offset = offsetAt(pos, this.originalText); - return offset >= this.tagInfo.start && offset <= this.tagInfo.end; + const offset = offsetAt(pos, this.originalText); + return offset >= this.tagInfo.start && offset <= this.tagInfo.end; } getURL(): string { - return this.url; + return this.url; } } export class SourceMapDocumentMapper implements DocumentMapper { - constructor( - protected consumer: SourceMapConsumer, - protected sourceUri: string, - private parent?: DocumentMapper - ) {} + constructor(protected consumer: SourceMapConsumer, protected sourceUri: string, private parent?: DocumentMapper) {} getOriginalPosition(generatedPosition: Position): Position { - if (this.parent) { - generatedPosition = this.parent.getOriginalPosition(generatedPosition); - } + if (this.parent) { + generatedPosition = this.parent.getOriginalPosition(generatedPosition); + } - if (generatedPosition.line < 0) { - return { line: -1, character: -1 }; - } + if (generatedPosition.line < 0) { + return { line: -1, character: -1 }; + } - const mapped = this.consumer.originalPositionFor({ - line: generatedPosition.line + 1, - column: generatedPosition.character - }); + const mapped = this.consumer.originalPositionFor({ + line: generatedPosition.line + 1, + column: generatedPosition.character, + }); - if (!mapped) { - return { line: -1, character: -1 }; - } + if (!mapped) { + return { line: -1, character: -1 }; + } - if (mapped.line === 0) { - console.log('Got 0 mapped line from', generatedPosition, 'col was', mapped.column); - } + if (mapped.line === 0) { + console.log('Got 0 mapped line from', generatedPosition, 'col was', mapped.column); + } - return { - line: (mapped.line || 0) - 1, - character: mapped.column || 0 - }; + return { + line: (mapped.line || 0) - 1, + character: mapped.column || 0, + }; } getGeneratedPosition(originalPosition: Position): Position { - if (this.parent) { - originalPosition = this.parent.getGeneratedPosition(originalPosition); - } + if (this.parent) { + originalPosition = this.parent.getGeneratedPosition(originalPosition); + } - const mapped = this.consumer.generatedPositionFor({ - line: originalPosition.line + 1, - column: originalPosition.character, - source: this.sourceUri - }); + const mapped = this.consumer.generatedPositionFor({ + line: originalPosition.line + 1, + column: originalPosition.character, + source: this.sourceUri, + }); - if (!mapped) { - return { line: -1, character: -1 }; - } + if (!mapped) { + return { line: -1, character: -1 }; + } - const result = { - line: (mapped.line || 0) - 1, - character: mapped.column || 0 - }; - - if (result.line < 0) { - return result; - } + const result = { + line: (mapped.line || 0) - 1, + character: mapped.column || 0, + }; + if (result.line < 0) { return result; + } + + return result; } isInGenerated(position: Position): boolean { - if (this.parent && !this.isInGenerated(position)) { - return false; - } + if (this.parent && !this.isInGenerated(position)) { + return false; + } - const generated = this.getGeneratedPosition(position); - return generated.line >= 0; + const generated = this.getGeneratedPosition(position); + return generated.line >= 0; } 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. */ destroy() { - this.parent?.destroy?.(); - this.consumer.destroy(); + this.parent?.destroy?.(); + this.consumer.destroy(); } } -export function mapRangeToOriginal( - fragment: Pick, - range: Range -): Range { +export function mapRangeToOriginal(fragment: Pick, range: Range): Range { // DON'T use Range.create here! Positions might not be mapped // and therefore return negative numbers, which makes Range.create throw. // These invalid position need to be handled // on a case-by-case basis in the calling functions. const originalRange = { - start: fragment.getOriginalPosition(range.start), - end: fragment.getOriginalPosition(range.end) + start: fragment.getOriginalPosition(range.start), + end: fragment.getOriginalPosition(range.end), }; // Range may be mapped one character short - reverse that for "in the same line" cases if ( - originalRange.start.line === originalRange.end.line && - range.start.line === range.end.line && - originalRange.end.character - originalRange.start.character === - range.end.character - range.start.character - 1 + originalRange.start.line === originalRange.end.line && + range.start.line === range.end.line && + originalRange.end.character - originalRange.start.character === range.end.character - range.start.character - 1 ) { - originalRange.end.character += 1; + originalRange.end.character += 1; } return originalRange; } export function mapRangeToGenerated(fragment: DocumentMapper, range: Range): Range { - return Range.create( - fragment.getGeneratedPosition(range.start), - fragment.getGeneratedPosition(range.end) - ); + return Range.create(fragment.getGeneratedPosition(range.start), fragment.getGeneratedPosition(range.end)); } -export function mapCompletionItemToOriginal( - fragment: Pick, - item: CompletionItem -): CompletionItem { +export function mapCompletionItemToOriginal(fragment: Pick, item: CompletionItem): CompletionItem { if (!item.textEdit) { - return item; + return item; } return { - ...item, - textEdit: mapEditToOriginal(fragment, item.textEdit) + ...item, + textEdit: mapEditToOriginal(fragment, item.textEdit), }; } -export function mapHoverToParent( - fragment: Pick, - hover: Hover -): Hover { +export function mapHoverToParent(fragment: Pick, hover: Hover): Hover { if (!hover.range) { - return hover; + return hover; } return { ...hover, range: mapRangeToOriginal(fragment, hover.range) }; } -export function mapObjWithRangeToOriginal( - fragment: Pick, - objWithRange: T -): T { +export function mapObjWithRangeToOriginal(fragment: Pick, objWithRange: T): T { return { ...objWithRange, range: mapRangeToOriginal(fragment, objWithRange.range) }; } -export function mapInsertReplaceEditToOriginal( - fragment: Pick, - edit: InsertReplaceEdit -): InsertReplaceEdit { +export function mapInsertReplaceEditToOriginal(fragment: Pick, edit: InsertReplaceEdit): InsertReplaceEdit { return { - ...edit, - insert: mapRangeToOriginal(fragment, edit.insert), - replace: mapRangeToOriginal(fragment, edit.replace) + ...edit, + insert: mapRangeToOriginal(fragment, edit.insert), + replace: mapRangeToOriginal(fragment, edit.replace), }; } -export function mapEditToOriginal( - fragment: Pick, - edit: TextEdit | InsertReplaceEdit -): TextEdit | InsertReplaceEdit { - return TextEdit.is(edit) - ? mapObjWithRangeToOriginal(fragment, edit) - : mapInsertReplaceEditToOriginal(fragment, edit); +export function mapEditToOriginal(fragment: Pick, edit: TextEdit | InsertReplaceEdit): TextEdit | InsertReplaceEdit { + return TextEdit.is(edit) ? mapObjWithRangeToOriginal(fragment, edit) : mapInsertReplaceEditToOriginal(fragment, edit); } -export function mapDiagnosticToGenerated( - fragment: DocumentMapper, - diagnostic: Diagnostic -): Diagnostic { +export function mapDiagnosticToGenerated(fragment: DocumentMapper, diagnostic: Diagnostic): Diagnostic { return { ...diagnostic, range: mapRangeToGenerated(fragment, diagnostic.range) }; } -export function mapColorPresentationToOriginal( - fragment: Pick, - presentation: ColorPresentation -): ColorPresentation { +export function mapColorPresentationToOriginal(fragment: Pick, presentation: ColorPresentation): ColorPresentation { const item = { - ...presentation + ...presentation, }; if (item.textEdit) { - item.textEdit = mapObjWithRangeToOriginal(fragment, item.textEdit); + item.textEdit = mapObjWithRangeToOriginal(fragment, item.textEdit); } if (item.additionalTextEdits) { - item.additionalTextEdits = item.additionalTextEdits.map((edit) => - mapObjWithRangeToOriginal(fragment, edit) - ); + item.additionalTextEdits = item.additionalTextEdits.map((edit) => mapObjWithRangeToOriginal(fragment, edit)); } return item; } -export function mapSymbolInformationToOriginal( - fragment: Pick, - info: SymbolInformation -): SymbolInformation { +export function mapSymbolInformationToOriginal(fragment: Pick, info: SymbolInformation): SymbolInformation { return { ...info, location: mapObjWithRangeToOriginal(fragment, info.location) }; } -export function mapLocationLinkToOriginal( - fragment: DocumentMapper, - def: LocationLink -): LocationLink { +export function mapLocationLinkToOriginal(fragment: DocumentMapper, def: LocationLink): LocationLink { return LocationLink.create( - def.targetUri, - fragment.getURL() === def.targetUri - ? mapRangeToOriginal(fragment, def.targetRange) - : def.targetRange, - fragment.getURL() === def.targetUri - ? mapRangeToOriginal(fragment, def.targetSelectionRange) - : def.targetSelectionRange, - def.originSelectionRange - ? mapRangeToOriginal(fragment, def.originSelectionRange) - : undefined + def.targetUri, + fragment.getURL() === def.targetUri ? mapRangeToOriginal(fragment, def.targetRange) : def.targetRange, + fragment.getURL() === def.targetUri ? mapRangeToOriginal(fragment, def.targetSelectionRange) : def.targetSelectionRange, + def.originSelectionRange ? mapRangeToOriginal(fragment, def.originSelectionRange) : undefined ); } export function mapTextDocumentEditToOriginal(fragment: DocumentMapper, edit: TextDocumentEdit) { if (edit.textDocument.uri !== fragment.getURL()) { - return edit; + return edit; } return TextDocumentEdit.create( - edit.textDocument, - edit.edits.map((textEdit) => mapObjWithRangeToOriginal(fragment, textEdit)) + edit.textDocument, + edit.edits.map((textEdit) => mapObjWithRangeToOriginal(fragment, textEdit)) ); } export function mapCodeActionToOriginal(fragment: DocumentMapper, codeAction: CodeAction) { return CodeAction.create( - codeAction.title, - { - documentChanges: codeAction.edit!.documentChanges!.map((edit) => - mapTextDocumentEditToOriginal(fragment, edit as TextDocumentEdit) - ) - }, - codeAction.kind + codeAction.title, + { + documentChanges: codeAction.edit!.documentChanges!.map((edit) => mapTextDocumentEditToOriginal(fragment, edit as TextDocumentEdit)), + }, + codeAction.kind ); } -export function mapSelectionRangeToParent( - fragment: Pick, - selectionRange: SelectionRange -): SelectionRange { +export function mapSelectionRangeToParent(fragment: Pick, selectionRange: SelectionRange): SelectionRange { const { range, parent } = selectionRange; - return SelectionRange.create( - mapRangeToOriginal(fragment, range), - parent && mapSelectionRangeToParent(fragment, parent) - ); -} \ No newline at end of file + return SelectionRange.create(mapRangeToOriginal(fragment, range), parent && mapSelectionRangeToParent(fragment, parent)); +} diff --git a/tools/astro-languageserver/src/core/documents/index.ts b/tools/astro-languageserver/src/core/documents/index.ts index 496107f3c..5dc0eb61f 100644 --- a/tools/astro-languageserver/src/core/documents/index.ts +++ b/tools/astro-languageserver/src/core/documents/index.ts @@ -2,4 +2,4 @@ export * from './Document'; export * from './DocumentBase'; export * from './DocumentManager'; export * from './DocumentMapper'; -export * from './utils'; \ No newline at end of file +export * from './utils'; diff --git a/tools/astro-languageserver/src/core/documents/utils.ts b/tools/astro-languageserver/src/core/documents/utils.ts index 220994f4c..227a76176 100644 --- a/tools/astro-languageserver/src/core/documents/utils.ts +++ b/tools/astro-languageserver/src/core/documents/utils.ts @@ -1,6 +1,6 @@ import { HTMLDocument, Node, Position } from 'vscode-html-languageservice'; import { clamp } from '../../utils'; -import {parseHtml} from './parseHtml'; +import { parseHtml } from './parseHtml'; export interface TagInformation { content: string; @@ -12,28 +12,23 @@ export interface TagInformation { container: { start: number; end: number }; } -function parseAttributes( - rawAttrs: Record | undefined -): Record { +function parseAttributes(rawAttrs: Record | undefined): Record { const attrs: Record = {}; if (!rawAttrs) { - return attrs; + return attrs; } Object.keys(rawAttrs).forEach((attrName) => { - const attrValue = rawAttrs[attrName]; - attrs[attrName] = attrValue === null ? attrName : removeOuterQuotes(attrValue); + const attrValue = rawAttrs[attrName]; + attrs[attrName] = attrValue === null ? attrName : removeOuterQuotes(attrValue); }); return attrs; function removeOuterQuotes(attrValue: string) { - if ( - (attrValue.startsWith('"') && attrValue.endsWith('"')) || - (attrValue.startsWith("'") && attrValue.endsWith("'")) - ) { - return attrValue.slice(1, attrValue.length - 1); - } - return attrValue; + if ((attrValue.startsWith('"') && attrValue.endsWith('"')) || (attrValue.startsWith("'") && attrValue.endsWith("'"))) { + return attrValue.slice(1, attrValue.length - 1); + } + return attrValue; } } @@ -163,8 +158,8 @@ function getLineOffsets(text: string) { } export function* walk(node: Node): Generator { - for(let child of node.children) { - yield * walk(child); + for (let child of node.children) { + yield* walk(child); } yield node; } @@ -202,18 +197,13 @@ export function* walk(node: Node, startIndex = 0) { * @param source text content to extract tag from * @param tag the tag to extract */ - function extractTags( - text: string, - tag: 'script' | 'style' | 'template', - html?: HTMLDocument -): TagInformation[] { +function extractTags(text: string, tag: 'script' | 'style' | 'template', html?: HTMLDocument): TagInformation[] { const rootNodes = html?.roots || parseHtml(text).roots; - const matchedNodes = rootNodes - .filter((node) => node.tag === tag); + const matchedNodes = rootNodes.filter((node) => node.tag === tag); - if(tag === 'style' && !matchedNodes.length && rootNodes.length && rootNodes[0].tag === 'html') { - for(let child of walk(rootNodes[0])) { - if(child.tag === 'style') { + if (tag === 'style' && !matchedNodes.length && rootNodes.length && rootNodes[0].tag === 'html') { + for (let child of walk(rootNodes[0])) { + if (child.tag === 'style') { matchedNodes.push(child); } } @@ -222,34 +212,34 @@ export function* walk(node: Node, startIndex = 0) { return matchedNodes.map(transformToTagInfo); function transformToTagInfo(matchedNode: Node) { - const start = matchedNode.startTagEnd ?? matchedNode.start; - const end = matchedNode.endTagStart ?? matchedNode.end; - const startPos = positionAt(start, text); - const endPos = positionAt(end, text); - const container = { - start: matchedNode.start, - end: matchedNode.end - }; - const content = text.substring(start, end); + const start = matchedNode.startTagEnd ?? matchedNode.start; + const end = matchedNode.endTagStart ?? matchedNode.end; + const startPos = positionAt(start, text); + const endPos = positionAt(end, text); + const container = { + start: matchedNode.start, + end: matchedNode.end, + }; + const content = text.substring(start, end); - return { - content, - attributes: parseAttributes(matchedNode.attributes), - start, - end, - startPos, - endPos, - container - }; + return { + content, + attributes: parseAttributes(matchedNode.attributes), + start, + end, + startPos, + endPos, + container, + }; } } export function extractStyleTag(source: string, html?: HTMLDocument): TagInformation | null { const styles = extractTags(source, 'style', html); if (!styles.length) { - return null; + return null; } // There can only be one style tag return styles[0]; -} \ No newline at end of file +} diff --git a/tools/astro-languageserver/src/plugins/css/CSSDocument.ts b/tools/astro-languageserver/src/plugins/css/CSSDocument.ts index 90b28352c..9f1839678 100644 --- a/tools/astro-languageserver/src/plugins/css/CSSDocument.ts +++ b/tools/astro-languageserver/src/plugins/css/CSSDocument.ts @@ -4,92 +4,92 @@ import { getLanguageService } from './service'; import { Document, DocumentMapper, ReadableDocument, TagInformation } from '../../core/documents/index'; export interface CSSDocumentBase extends DocumentMapper, TextDocument { - languageId: string; - stylesheet: Stylesheet; + languageId: string; + stylesheet: Stylesheet; } export class CSSDocument extends ReadableDocument implements DocumentMapper { - private styleInfo: Pick; - readonly version = this.parent.version; + private styleInfo: Pick; + readonly version = this.parent.version; - public stylesheet: Stylesheet; - public languageId: string; + public stylesheet: Stylesheet; + public languageId: string; - constructor(private parent: Document) { - super(); + constructor(private parent: Document) { + super(); - if (this.parent.styleInfo) { - this.styleInfo = this.parent.styleInfo; - } else { - this.styleInfo = { - attributes: {}, - start: -1, - end: -1 - }; - } - - this.languageId = this.language; - this.stylesheet = getLanguageService(this.language).parseStylesheet(this); + if (this.parent.styleInfo) { + this.styleInfo = this.parent.styleInfo; + } else { + this.styleInfo = { + attributes: {}, + start: -1, + end: -1, + }; } - /** - * Get the fragment position relative to the parent - * @param pos Position in fragment - */ - getOriginalPosition(pos: Position): Position { - const parentOffset = this.styleInfo.start + this.offsetAt(pos); - return this.parent.positionAt(parentOffset); - } + this.languageId = this.language; + this.stylesheet = getLanguageService(this.language).parseStylesheet(this); + } - /** - * Get the position relative to the start of the fragment - * @param pos Position in parent - */ - getGeneratedPosition(pos: Position): Position { - const fragmentOffset = this.parent.offsetAt(pos) - this.styleInfo.start; - return this.positionAt(fragmentOffset); - } + /** + * Get the fragment position relative to the parent + * @param pos Position in fragment + */ + getOriginalPosition(pos: Position): Position { + const parentOffset = this.styleInfo.start + this.offsetAt(pos); + return this.parent.positionAt(parentOffset); + } - /** - * Returns true if the given parent position is inside of this fragment - * @param pos Position in parent - */ - isInGenerated(pos: Position): boolean { - const offset = this.parent.offsetAt(pos); - return offset >= this.styleInfo.start && offset <= this.styleInfo.end; - } + /** + * Get the position relative to the start of the fragment + * @param pos Position in parent + */ + getGeneratedPosition(pos: Position): Position { + const fragmentOffset = this.parent.offsetAt(pos) - this.styleInfo.start; + return this.positionAt(fragmentOffset); + } - /** - * Get the fragment text from the parent - */ - getText(): string { - return this.parent.getText().slice(this.styleInfo.start, this.styleInfo.end); - } + /** + * Returns true if the given parent position is inside of this fragment + * @param pos Position in parent + */ + isInGenerated(pos: Position): boolean { + const offset = this.parent.offsetAt(pos); + return offset >= this.styleInfo.start && offset <= this.styleInfo.end; + } - /** - * Returns the length of the fragment as calculated from the start and end positon - */ - getTextLength(): number { - return this.styleInfo.end - this.styleInfo.start; - } + /** + * Get the fragment text from the parent + */ + getText(): string { + return this.parent.getText().slice(this.styleInfo.start, this.styleInfo.end); + } - /** - * Return the parent file path - */ - getFilePath(): string | null { - return this.parent.getFilePath(); - } + /** + * Returns the length of the fragment as calculated from the start and end positon + */ + getTextLength(): number { + 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() { - return this.styleInfo.attributes; - } + getURL() { + return this.parent.getURL(); + } - private get language() { - const attrs = this.getAttributes(); - return attrs.lang || attrs.type || 'css'; - } + getAttributes() { + return this.styleInfo.attributes; + } + + private get language() { + const attrs = this.getAttributes(); + return attrs.lang || attrs.type || 'css'; + } } diff --git a/tools/astro-languageserver/src/plugins/css/CSSPlugin.ts b/tools/astro-languageserver/src/plugins/css/CSSPlugin.ts index 4c0dcb949..26c90ac66 100644 --- a/tools/astro-languageserver/src/plugins/css/CSSPlugin.ts +++ b/tools/astro-languageserver/src/plugins/css/CSSPlugin.ts @@ -26,24 +26,16 @@ export class CSSPlugin implements CompletionsProvider { }); } - getCompletions( - document: Document, - position: Position, - completionContext?: CompletionContext - ): CompletionList | null { + getCompletions(document: Document, position: Position, completionContext?: CompletionContext): CompletionList | null { const triggerCharacter = completionContext?.triggerCharacter; const triggerKind = completionContext?.triggerKind; const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter; - if ( - isCustomTriggerCharacter && - triggerCharacter && - !this.triggerCharacters.has(triggerCharacter) - ) { - return null; + if (isCustomTriggerCharacter && triggerCharacter && !this.triggerCharacters.has(triggerCharacter)) { + return null; } - if(this.isInsideFrontmatter(document, position)) { + if (this.isInsideFrontmatter(document, position)) { return null; } @@ -55,82 +47,55 @@ export class CSSPlugin implements CompletionsProvider { const attributeContext = getAttributeContextAtPosition(document, position); if (!attributeContext) { - return null; + return null; } if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) { - const [start, end] = attributeContext.valueRange; - return this.getCompletionsInternal( - document, - position, - new StyleAttributeDocument(document, start, end) - ); + const [start, end] = attributeContext.valueRange; + return this.getCompletionsInternal(document, position, new StyleAttributeDocument(document, start, end)); } else { - return getIdClassCompletion(cssDocument, attributeContext); + return getIdClassCompletion(cssDocument, attributeContext); } } - private getCompletionsInternal( - document: Document, - position: Position, - cssDocument: CSSDocumentBase - ) { - if (isSASS(cssDocument)) { - // the css language service does not support sass, still we can use - // the emmet helper directly to at least get emmet completions - return doEmmetComplete(document, position, 'sass', this.configManager.getEmmetConfig()); - } + private getCompletionsInternal(document: Document, position: Position, cssDocument: CSSDocumentBase) { + if (isSASS(cssDocument)) { + // the css language service does not support sass, still we can use + // the emmet helper directly to at least get emmet completions + return doEmmetComplete(document, position, 'sass', this.configManager.getEmmetConfig()); + } - const type = extractLanguage(cssDocument); + const type = extractLanguage(cssDocument); - const lang = getLanguageService(type); - const emmetResults: CompletionList = { - isIncomplete: true, - items: [] - }; - if (false /* this.configManager.getConfig().css.completions.emmet */) { - lang.setCompletionParticipants([ - getEmmetCompletionParticipants( - cssDocument, - cssDocument.getGeneratedPosition(position), - getLanguage(type), - this.configManager.getEmmetConfig(), - emmetResults - ) - ]); - } - const results = lang.doComplete( - cssDocument, - cssDocument.getGeneratedPosition(position), - cssDocument.stylesheet - ); - return CompletionList.create( - [...(results ? results.items : []), ...emmetResults.items].map((completionItem) => - mapCompletionItemToOriginal(cssDocument, completionItem) - ), - // Emmet completions change on every keystroke, so they are never complete - emmetResults.items.length > 0 - ); - } - - private inStyleAttributeWithoutInterpolation( - attrContext: AttributeContext, - text: string - ): attrContext is Required { - return ( - attrContext.name === 'style' && - !!attrContext.valueRange && - !text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{') + const lang = getLanguageService(type); + const emmetResults: CompletionList = { + isIncomplete: true, + items: [], + }; + if (false /* this.configManager.getConfig().css.completions.emmet */) { + lang.setCompletionParticipants([ + getEmmetCompletionParticipants(cssDocument, cssDocument.getGeneratedPosition(position), getLanguage(type), this.configManager.getEmmetConfig(), emmetResults), + ]); + } + const results = lang.doComplete(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet); + return CompletionList.create( + [...(results ? results.items : []), ...emmetResults.items].map((completionItem) => mapCompletionItemToOriginal(cssDocument, completionItem)), + // Emmet completions change on every keystroke, so they are never complete + emmetResults.items.length > 0 ); } + private inStyleAttributeWithoutInterpolation(attrContext: AttributeContext, text: string): attrContext is Required { + return attrContext.name === 'style' && !!attrContext.valueRange && !text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{'); + } + private getCSSDoc(document: Document) { - let cssDoc = this.documents.get(document); - if (!cssDoc || cssDoc.version < document.version) { - cssDoc = new CSSDocument(document); - this.documents.set(document, cssDoc); - } - return cssDoc; + let cssDoc = this.documents.get(document); + if (!cssDoc || cssDoc.version < document.version) { + cssDoc = new CSSDocument(document); + this.documents.set(document, cssDoc); + } + return cssDoc; } private isInsideFrontmatter(document: Document, position: Position) { @@ -140,14 +105,14 @@ export class CSSPlugin implements CompletionsProvider { function isSASS(document: CSSDocumentBase) { switch (extractLanguage(document)) { - case 'sass': - return true; - default: - return false; + case 'sass': + return true; + default: + return false; } } function extractLanguage(document: CSSDocumentBase): string { const lang = document.languageId; return lang.replace(/^text\//, ''); -} \ No newline at end of file +} diff --git a/tools/astro-languageserver/src/plugins/css/StyleAttributeDocument.ts b/tools/astro-languageserver/src/plugins/css/StyleAttributeDocument.ts index 7b49d771d..e00398037 100644 --- a/tools/astro-languageserver/src/plugins/css/StyleAttributeDocument.ts +++ b/tools/astro-languageserver/src/plugins/css/StyleAttributeDocument.ts @@ -7,70 +7,66 @@ const PREFIX = '__ {'; const SUFFIX = '}'; export class StyleAttributeDocument extends ReadableDocument implements DocumentMapper { - readonly version = this.parent.version; + readonly version = this.parent.version; - public stylesheet: Stylesheet; - public languageId = 'css'; + public stylesheet: Stylesheet; + public languageId = 'css'; - constructor( - private readonly parent: Document, - private readonly attrStart: number, - private readonly attrEnd: number - ) { - super(); + constructor(private readonly parent: Document, 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 - * @param pos Position in fragment - */ - getOriginalPosition(pos: Position): Position { - const parentOffset = this.attrStart + this.offsetAt(pos) - PREFIX.length; - return this.parent.positionAt(parentOffset); - } + /** + * Get the fragment position relative to the parent + * @param pos Position in fragment + */ + getOriginalPosition(pos: Position): Position { + const parentOffset = this.attrStart + this.offsetAt(pos) - PREFIX.length; + return this.parent.positionAt(parentOffset); + } - /** - * Get the position relative to the start of the fragment - * @param pos Position in parent - */ - getGeneratedPosition(pos: Position): Position { - const fragmentOffset = this.parent.offsetAt(pos) - this.attrStart + PREFIX.length; - return this.positionAt(fragmentOffset); - } + /** + * Get the position relative to the start of the fragment + * @param pos Position in parent + */ + getGeneratedPosition(pos: Position): Position { + const fragmentOffset = this.parent.offsetAt(pos) - this.attrStart + PREFIX.length; + return this.positionAt(fragmentOffset); + } - /** - * Returns true if the given parent position is inside of this fragment - * @param pos Position in parent - */ - isInGenerated(pos: Position): boolean { - const offset = this.parent.offsetAt(pos); - return offset >= this.attrStart && offset <= this.attrEnd; - } + /** + * Returns true if the given parent position is inside of this fragment + * @param pos Position in parent + */ + isInGenerated(pos: Position): boolean { + const offset = this.parent.offsetAt(pos); + return offset >= this.attrStart && offset <= this.attrEnd; + } - /** - * Get the fragment text from the parent - */ - getText(): string { - return PREFIX + this.parent.getText().slice(this.attrStart, this.attrEnd) + SUFFIX; - } + /** + * Get the fragment text from the parent + */ + getText(): string { + return PREFIX + this.parent.getText().slice(this.attrStart, this.attrEnd) + SUFFIX; + } - /** - * Returns the length of the fragment as calculated from the start and end position - */ - getTextLength(): number { - return PREFIX.length + this.attrEnd - this.attrStart + SUFFIX.length; - } + /** + * Returns the length of the fragment as calculated from the start and end position + */ + getTextLength(): number { + return PREFIX.length + this.attrEnd - this.attrStart + SUFFIX.length; + } - /** - * Return the parent file path - */ - getFilePath(): string | null { - return this.parent.getFilePath(); - } + /** + * Return the parent file path + */ + getFilePath(): string | null { + return this.parent.getFilePath(); + } - getURL() { - return this.parent.getURL(); - } -} \ No newline at end of file + getURL() { + return this.parent.getURL(); + } +} diff --git a/tools/astro-languageserver/src/plugins/css/features/getIdClassCompletion.ts b/tools/astro-languageserver/src/plugins/css/features/getIdClassCompletion.ts index 368359ac9..45acb5ad6 100644 --- a/tools/astro-languageserver/src/plugins/css/features/getIdClassCompletion.ts +++ b/tools/astro-languageserver/src/plugins/css/features/getIdClassCompletion.ts @@ -2,32 +2,29 @@ import { CompletionItem, CompletionItemKind, CompletionList } from 'vscode-langu import { AttributeContext } from '../../../core/documents/parseHtml'; import { CSSDocument } from '../CSSDocument'; -export function getIdClassCompletion( - cssDoc: CSSDocument, - attributeContext: AttributeContext -): CompletionList | null { - const collectingType = getCollectingType(attributeContext); +export function getIdClassCompletion(cssDoc: CSSDocument, attributeContext: AttributeContext): CompletionList | null { + const collectingType = getCollectingType(attributeContext); - if (!collectingType) { - return null; - } - const items = collectSelectors(cssDoc.stylesheet as CSSNode, collectingType); + if (!collectingType) { + return null; + } + const items = collectSelectors(cssDoc.stylesheet as CSSNode, collectingType); - console.log("getIdClassCompletion items", items.length); - return CompletionList.create(items); + console.log('getIdClassCompletion items', items.length); + return CompletionList.create(items); } function getCollectingType(attributeContext: AttributeContext): number | undefined { - if (attributeContext.inValue) { - if (attributeContext.name === 'class') { - return NodeType.ClassSelector; - } - if (attributeContext.name === 'id') { - return NodeType.IdentifierSelector; - } - } else if (attributeContext.name.startsWith('class:')) { - return NodeType.ClassSelector; + if (attributeContext.inValue) { + if (attributeContext.name === 'class') { + return NodeType.ClassSelector; } + if (attributeContext.name === 'id') { + return NodeType.IdentifierSelector; + } + } else if (attributeContext.name.startsWith('class:')) { + return NodeType.ClassSelector; + } } /** @@ -36,35 +33,35 @@ function getCollectingType(attributeContext: AttributeContext): number | undefin * The enum is not exported. we have to update this whenever it changes */ export enum NodeType { - ClassSelector = 14, - IdentifierSelector = 15 + ClassSelector = 14, + IdentifierSelector = 15, } export type CSSNode = { - type: number; - children: CSSNode[] | undefined; - getText(): string; + type: number; + children: CSSNode[] | undefined; + getText(): string; }; export function collectSelectors(stylesheet: CSSNode, type: number) { - const result: CSSNode[] = []; - walk(stylesheet, (node) => { - if (node.type === type) { - result.push(node); - } - }); + const result: CSSNode[] = []; + walk(stylesheet, (node) => { + if (node.type === type) { + result.push(node); + } + }); - return result.map( - (node): CompletionItem => ({ - label: node.getText().substring(1), - kind: CompletionItemKind.Keyword - }) - ); + return result.map( + (node): CompletionItem => ({ + label: node.getText().substring(1), + kind: CompletionItemKind.Keyword, + }) + ); } function walk(node: CSSNode, callback: (node: CSSNode) => void) { - callback(node); - if (node.children) { - node.children.forEach((node) => walk(node, callback)); - } -} \ No newline at end of file + callback(node); + if (node.children) { + node.children.forEach((node) => walk(node, callback)); + } +} diff --git a/tools/astro-languageserver/src/plugins/css/service.ts b/tools/astro-languageserver/src/plugins/css/service.ts index e8ac86a65..78b11296e 100644 --- a/tools/astro-languageserver/src/plugins/css/service.ts +++ b/tools/astro-languageserver/src/plugins/css/service.ts @@ -1,58 +1,48 @@ -import { - getCSSLanguageService, - getSCSSLanguageService, - getLESSLanguageService, - LanguageService, - ICSSDataProvider -} from 'vscode-css-languageservice'; +import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageService, ICSSDataProvider } from 'vscode-css-languageservice'; const customDataProvider: ICSSDataProvider = { providePseudoClasses() { - return []; + return []; }, provideProperties() { - return []; + return []; }, provideAtDirectives() { - return []; + return []; }, providePseudoElements() { - return []; - } + return []; + }, }; -const [css, scss, less] = [ - getCSSLanguageService, - getSCSSLanguageService, - getLESSLanguageService -].map((getService) => +const [css, scss, less] = [getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService].map((getService) => getService({ - customDataProviders: [customDataProvider] + customDataProviders: [customDataProvider], }) ); const langs = { css, scss, - less + less, }; export function getLanguage(kind?: string) { switch (kind) { - case 'scss': - case 'text/scss': - return 'scss' as const; - case 'less': - case 'text/less': - return 'less' as const; - case 'css': - case 'text/css': - default: - return 'css' as const; + case 'scss': + case 'text/scss': + return 'scss' as const; + case 'less': + case 'text/less': + return 'less' as const; + case 'css': + case 'text/css': + default: + return 'css' as const; } } export function getLanguageService(kind?: string): LanguageService { const lang = getLanguage(kind); return langs[lang]; -} \ No newline at end of file +} diff --git a/tools/astro-languageserver/src/plugins/index.ts b/tools/astro-languageserver/src/plugins/index.ts index 368b339cb..bb73cbe5e 100644 --- a/tools/astro-languageserver/src/plugins/index.ts +++ b/tools/astro-languageserver/src/plugins/index.ts @@ -3,4 +3,4 @@ export * from './astro/AstroPlugin'; export * from './html/HTMLPlugin'; export * from './typescript/TypeScriptPlugin'; export * from './interfaces'; -export * from './css/CSSPlugin'; \ No newline at end of file +export * from './css/CSSPlugin';