diff options
Diffstat (limited to 'tools/vscode/packages/server/src')
19 files changed, 1131 insertions, 1375 deletions
diff --git a/tools/vscode/packages/server/src/core/config/ConfigManager.ts b/tools/vscode/packages/server/src/core/config/ConfigManager.ts index 4c1c23b13..1e795ab96 100644 --- a/tools/vscode/packages/server/src/core/config/ConfigManager.ts +++ b/tools/vscode/packages/server/src/core/config/ConfigManager.ts @@ -1,13 +1,13 @@ import { VSCodeEmmetConfig } from 'vscode-emmet-helper'; export class ConfigManager { - private emmetConfig: VSCodeEmmetConfig = {}; - - updateEmmetConfig(config: VSCodeEmmetConfig): void { - this.emmetConfig = config || {}; - } + private emmetConfig: VSCodeEmmetConfig = {}; - getEmmetConfig(): VSCodeEmmetConfig { - return this.emmetConfig; - } + updateEmmetConfig(config: VSCodeEmmetConfig): void { + this.emmetConfig = config || {}; + } + + getEmmetConfig(): VSCodeEmmetConfig { + return this.emmetConfig; + } } diff --git a/tools/vscode/packages/server/src/core/documents/Document.ts b/tools/vscode/packages/server/src/core/documents/Document.ts index 4f90813ee..93217e891 100644 --- a/tools/vscode/packages/server/src/core/documents/Document.ts +++ b/tools/vscode/packages/server/src/core/documents/Document.ts @@ -7,153 +7,147 @@ import { parseHtml } from './parseHtml'; import { parseAstro, AstroDocument } from './parseAstro'; export class Document implements TextDocument { - - private content: string; - - languageId = 'astro'; - version = 0; - html!: HTMLDocument; - astro!: AstroDocument; - - constructor(public uri: string, text: string) { - this.content = text; - this.updateDocInfo(); + private content: string; + + languageId = 'astro'; + version = 0; + html!: HTMLDocument; + astro!: AstroDocument; + + constructor(public uri: string, text: string) { + this.content = text; + this.updateDocInfo(); + } + + private updateDocInfo() { + this.html = parseHtml(this.content); + this.astro = parseAstro(this.content); + } + + setText(text: string) { + this.content = text; + this.version++; + this.updateDocInfo(); + } + + /** + * 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)); + } + + getText(): string { + return this.content; + } + + /** + * 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); } - private updateDocInfo() { - this.html = parseHtml(this.content); - this.astro = parseAstro(this.content); + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (lineOffsets[mid] > offset) { + high = mid; + } else { + low = mid + 1; + } } - setText(text: string) { - this.content = text; - this.version++; - this.updateDocInfo(); + // low is the least x for which the line offset is larger than the current offset + // or array.length if no line offset is larger than the current offset + const line = low - 1; + return Position.create(line, offset - lineOffsets[line]); + } + + /** + * Get the index of the line and character position + * @param position Line and character position + */ + offsetAt(position: Position): number { + const lineOffsets = this.getLineOffsets(); + + if (position.line >= lineOffsets.length) { + return this.getTextLength(); + } else if (position.line < 0) { + return 0; } - /** - * 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)); + 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); + } + + getLineUntilOffset(offset: number): string { + const { line, character } = this.positionAt(offset); + return this.lines[line].slice(0, character); + } + + private getLineOffsets() { + const lineOffsets = []; + const text = this.getText(); + let isLineStart = true; + + for (let i = 0; i < text.length; i++) { + if (isLineStart) { + lineOffsets.push(i); + isLineStart = false; + } + const ch = text.charAt(i); + isLineStart = ch === '\r' || ch === '\n'; + if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { + i++; + } } - getText(): string { - return this.content - } - - /** - * Get the line and character based on the offset - * @param offset The index of the position - */ - positionAt(offset: number): Position { - offset = clamp(offset, 0, this.getTextLength()); - - const lineOffsets = this.getLineOffsets(); - let low = 0; - let high = lineOffsets.length; - if (high === 0) { - return Position.create(0, offset); - } - - while (low < high) { - const mid = Math.floor((low + high) / 2); - if (lineOffsets[mid] > offset) { - high = mid; - } else { - low = mid + 1; - } - } - - // low is the least x for which the line offset is larger than the current offset - // or array.length if no line offset is larger than the current offset - const line = low - 1; - return Position.create(line, offset - lineOffsets[line]); - } - - /** - * Get the index of the line and character position - * @param position Line and character position - */ - offsetAt(position: Position): number { - const lineOffsets = this.getLineOffsets(); - - if (position.line >= lineOffsets.length) { - return this.getTextLength(); - } else if (position.line < 0) { - return 0; - } - - const lineOffset = lineOffsets[position.line]; - const nextLineOffset = - position.line + 1 < lineOffsets.length - ? lineOffsets[position.line + 1] - : this.getTextLength(); - - return clamp(nextLineOffset, lineOffset, lineOffset + position.character); - } - - getLineUntilOffset(offset: number): string { - const { line, character } = this.positionAt(offset); - return this.lines[line].slice(0, character); - } - - private getLineOffsets() { - const lineOffsets = []; - const text = this.getText(); - let isLineStart = true; - - for (let i = 0; i < text.length; i++) { - if (isLineStart) { - lineOffsets.push(i); - isLineStart = false; - } - const ch = text.charAt(i); - isLineStart = ch === '\r' || ch === '\n'; - if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { - i++; - } - } - - if (isLineStart && text.length > 0) { - lineOffsets.push(text.length); - } - - return lineOffsets; - } - - /** - * Get the length of the document's content - */ - getTextLength(): number { - return this.getText().length; - } - - /** - * Returns the file path if the url scheme is file - */ - getFilePath(): string | null { - return urlToPath(this.uri); - } - - /** - * Get URL file path. - */ - getURL() { - return this.uri; - } - - - get lines(): string[] { - return this.getText().split(/\r?\n/); - } - - get lineCount(): number { - return this.lines.length; + if (isLineStart && text.length > 0) { + lineOffsets.push(text.length); } + return lineOffsets; + } + + /** + * Get the length of the document's content + */ + getTextLength(): number { + return this.getText().length; + } + + /** + * Returns the file path if the url scheme is file + */ + getFilePath(): string | null { + return urlToPath(this.uri); + } + + /** + * Get URL file path. + */ + getURL() { + return this.uri; + } + + get lines(): string[] { + return this.getText().split(/\r?\n/); + } + + get lineCount(): number { + return this.lines.length; + } } diff --git a/tools/vscode/packages/server/src/core/documents/DocumentManager.ts b/tools/vscode/packages/server/src/core/documents/DocumentManager.ts index 6195514d8..7c9c168c1 100644 --- a/tools/vscode/packages/server/src/core/documents/DocumentManager.ts +++ b/tools/vscode/packages/server/src/core/documents/DocumentManager.ts @@ -1,104 +1,94 @@ import { EventEmitter } from 'events'; -import { - TextDocumentContentChangeEvent, - TextDocumentItem -} from 'vscode-languageserver'; +import { TextDocumentContentChangeEvent, TextDocumentItem } from 'vscode-languageserver'; import { Document } from './Document'; import { normalizeUri } from '../../utils'; export type DocumentEvent = 'documentOpen' | 'documentChange' | 'documentClose'; export class DocumentManager { - private emitter = new EventEmitter(); - private openedInClient = new Set<string>(); - private documents: Map<string, Document> = new Map(); - private locked = new Set<string>(); - private deleteCandidates = new Set<string>(); - - constructor( - private createDocument: (textDocument: { uri: string, text: string }) => Document - ) {} - - get(uri: string) { - return this.documents.get(normalizeUri(uri)); + private emitter = new EventEmitter(); + private openedInClient = new Set<string>(); + private documents: Map<string, Document> = new Map(); + private locked = new Set<string>(); + private deleteCandidates = new Set<string>(); + + constructor(private createDocument: (textDocument: { uri: string; text: string }) => Document) {} + + get(uri: string) { + return this.documents.get(normalizeUri(uri)); + } + + openDocument(textDocument: TextDocumentItem) { + let document: Document; + if (this.documents.has(textDocument.uri)) { + document = this.get(textDocument.uri) as Document; + document.setText(textDocument.text); + } else { + document = this.createDocument(textDocument); + this.documents.set(normalizeUri(textDocument.uri), document); + this.notify('documentOpen', document); } - openDocument(textDocument: TextDocumentItem) { - let document: Document; - if (this.documents.has(textDocument.uri)) { - document = this.get(textDocument.uri) as Document; - document.setText(textDocument.text); - } else { - document = this.createDocument(textDocument); - this.documents.set(normalizeUri(textDocument.uri), document); - this.notify('documentOpen', document); - } + this.notify('documentChange', document); - this.notify('documentChange', document); + return document; + } - return document; - } + closeDocument(uri: string) { + uri = normalizeUri(uri); - closeDocument(uri: string) { - uri = normalizeUri(uri); + const document = this.documents.get(uri); + if (!document) { + throw new Error('Cannot call methods on an unopened document'); + } - const document = this.documents.get(uri); - if (!document) { - throw new Error('Cannot call methods on an unopened document'); - } + this.notify('documentClose', document); - this.notify('documentClose', document); + // Some plugin may prevent a document from actually being closed. + if (!this.locked.has(uri)) { + this.documents.delete(uri); + } else { + this.deleteCandidates.add(uri); + } - // Some plugin may prevent a document from actually being closed. - if (!this.locked.has(uri)) { - this.documents.delete(uri); - } else { - this.deleteCandidates.add(uri); - } + this.openedInClient.delete(uri); + } - this.openedInClient.delete(uri); + updateDocument(uri: string, changes: TextDocumentContentChangeEvent[]) { + const document = this.documents.get(normalizeUri(uri)); + if (!document) { + throw new Error('Cannot call methods on an unopened document'); } - updateDocument( - uri: string, - changes: TextDocumentContentChangeEvent[] - ) { - const document = this.documents.get(normalizeUri(uri)); - if (!document) { - throw new Error('Cannot call methods on an unopened document'); - } - - for (const change of changes) { - let start = 0; - let end = 0; - if ('range' in change) { - start = document.offsetAt(change.range.start); - end = document.offsetAt(change.range.end); - } else { - end = document.getTextLength(); - } - - document.update(change.text, start, end); - } - - this.notify('documentChange', document); + for (const change of changes) { + let start = 0; + let end = 0; + if ('range' in change) { + start = document.offsetAt(change.range.start); + end = document.offsetAt(change.range.end); + } else { + end = document.getTextLength(); + } + + document.update(change.text, start, end); } - markAsOpenedInClient(uri: string) { - this.openedInClient.add(normalizeUri(uri)); - } + this.notify('documentChange', document); + } - getAllOpenedByClient() { - return Array.from(this.documents.entries()).filter((doc) => - this.openedInClient.has(doc[0]) - ); - } + markAsOpenedInClient(uri: string) { + this.openedInClient.add(normalizeUri(uri)); + } - on(name: DocumentEvent, listener: (document: Document) => void) { - this.emitter.on(name, listener); - } + getAllOpenedByClient() { + return Array.from(this.documents.entries()).filter((doc) => this.openedInClient.has(doc[0])); + } - private notify(name: DocumentEvent, document: Document) { - this.emitter.emit(name, document); - } + on(name: DocumentEvent, listener: (document: Document) => void) { + this.emitter.on(name, listener); + } + + private notify(name: DocumentEvent, document: Document) { + this.emitter.emit(name, document); + } } diff --git a/tools/vscode/packages/server/src/core/documents/parseAstro.ts b/tools/vscode/packages/server/src/core/documents/parseAstro.ts index e4f71721a..71c7764d8 100644 --- a/tools/vscode/packages/server/src/core/documents/parseAstro.ts +++ b/tools/vscode/packages/server/src/core/documents/parseAstro.ts @@ -1,74 +1,77 @@ import { getFirstNonWhitespaceIndex } from './utils'; interface Frontmatter { - state: null | 'open' | 'closed'; - startOffset: null | number; - endOffset: null | number; + state: null | 'open' | 'closed'; + startOffset: null | number; + endOffset: null | number; } interface Content { - firstNonWhitespaceOffset: null | number; + firstNonWhitespaceOffset: null | number; } export interface AstroDocument { - frontmatter: Frontmatter - content: Content; + frontmatter: Frontmatter; + content: Content; } /** Parses a document to collect metadata about Astro features */ export function parseAstro(content: string): AstroDocument { - const frontmatter = getFrontmatter(content) - return { - frontmatter, - content: getContent(content, frontmatter) - } + const frontmatter = getFrontmatter(content); + return { + frontmatter, + content: getContent(content, frontmatter), + }; } /** Get frontmatter metadata */ function getFrontmatter(content: string): Frontmatter { - /** Quickly check how many `---` blocks are in the document */ - function getFrontmatterState(): Frontmatter['state'] { - const parts = content.trim().split('---').length; - switch (parts) { - case 1: return null; - case 2: return 'open'; - default: return 'closed'; - } + /** Quickly check how many `---` blocks are in the document */ + function getFrontmatterState(): Frontmatter['state'] { + const parts = content.trim().split('---').length; + switch (parts) { + case 1: + return null; + case 2: + return 'open'; + default: + return 'closed'; } - const state = getFrontmatterState(); + } + const state = getFrontmatterState(); - /** Construct a range containing the document's frontmatter */ - function getFrontmatterOffsets(): [number|null, number|null] { - const startOffset = content.indexOf('---'); - if (startOffset === -1) return [null, null]; - const endOffset = content.slice(startOffset + 3).indexOf('---') + 3; - if (endOffset === -1) return [startOffset, null]; - return [startOffset, endOffset]; - } - const [startOffset, endOffset] = getFrontmatterOffsets(); + /** Construct a range containing the document's frontmatter */ + function getFrontmatterOffsets(): [number | null, number | null] { + const startOffset = content.indexOf('---'); + if (startOffset === -1) return [null, null]; + const endOffset = content.slice(startOffset + 3).indexOf('---') + 3; + if (endOffset === -1) return [startOffset, null]; + return [startOffset, endOffset]; + } + const [startOffset, endOffset] = getFrontmatterOffsets(); - return { - state, - startOffset, - endOffset - }; + return { + state, + startOffset, + endOffset, + }; } /** Get content metadata */ function getContent(content: string, frontmatter: Frontmatter): Content { - switch (frontmatter.state) { - case null: { - const offset = getFirstNonWhitespaceIndex(content); - return { firstNonWhitespaceOffset: offset === -1 ? null : offset } - } - case 'open': { - return { firstNonWhitespaceOffset: null } - } - case 'closed': { - const { endOffset } = frontmatter; - const end = (endOffset ?? 0) + 3; - const offset = getFirstNonWhitespaceIndex(content.slice(end)) - return { firstNonWhitespaceOffset: end + offset } - } + switch (frontmatter.state) { + case null: { + const offset = getFirstNonWhitespaceIndex(content); + return { firstNonWhitespaceOffset: offset === -1 ? null : offset }; + } + case 'open': { + return { firstNonWhitespaceOffset: null }; + } + case 'closed': { + const { endOffset } = frontmatter; + const end = (endOffset ?? 0) + 3; + const offset = getFirstNonWhitespaceIndex(content.slice(end)); + return { firstNonWhitespaceOffset: end + offset }; } + } } diff --git a/tools/vscode/packages/server/src/core/documents/parseHtml.ts b/tools/vscode/packages/server/src/core/documents/parseHtml.ts index 86af06008..f5de5f292 100644 --- a/tools/vscode/packages/server/src/core/documents/parseHtml.ts +++ b/tools/vscode/packages/server/src/core/documents/parseHtml.ts @@ -1,12 +1,4 @@ -import { - getLanguageService, - HTMLDocument, - TokenType, - ScannerState, - Scanner, - Node, - Position -} from 'vscode-html-languageservice'; +import { getLanguageService, HTMLDocument, TokenType, ScannerState, Scanner, Node, Position } from 'vscode-html-languageservice'; import { Document } from './Document'; import { isInsideExpression } from './utils'; @@ -16,154 +8,134 @@ const parser = getLanguageService(); * Parses text as HTML */ export function parseHtml(text: string): HTMLDocument { - const preprocessed = preprocess(text); + const preprocessed = preprocess(text); - // We can safely only set getText because only this is used for parsing - const parsedDoc = parser.parseHTMLDocument(<any>{ getText: () => preprocessed }); + // We can safely only set getText because only this is used for parsing + const parsedDoc = parser.parseHTMLDocument(<any>{ getText: () => preprocessed }); - return parsedDoc; + return parsedDoc; } -const createScanner = parser.createScanner as ( - input: string, - initialOffset?: number, - initialState?: ScannerState -) => Scanner; +const createScanner = parser.createScanner as (input: string, initialOffset?: number, initialState?: ScannerState) => Scanner; /** * scan the text and remove any `>` or `<` that cause the tag to end short, */ function preprocess(text: string) { - let scanner = createScanner(text); - let token = scanner.scan(); - let currentStartTagStart: number | null = null; + let scanner = createScanner(text); + let token = scanner.scan(); + let currentStartTagStart: number | null = null; - while (token !== TokenType.EOS) { - const offset = scanner.getTokenOffset(); + while (token !== TokenType.EOS) { + const offset = scanner.getTokenOffset(); - if (token === TokenType.StartTagOpen) { - currentStartTagStart = offset; - } - - if (token === TokenType.StartTagClose) { - if (shouldBlankStartOrEndTagLike(offset)) { - blankStartOrEndTagLike(offset); - } else { - currentStartTagStart = null; - } - } + if (token === TokenType.StartTagOpen) { + currentStartTagStart = offset; + } - if (token === TokenType.StartTagSelfClose) { - currentStartTagStart = null; - } + if (token === TokenType.StartTagClose) { + if (shouldBlankStartOrEndTagLike(offset)) { + blankStartOrEndTagLike(offset); + } else { + currentStartTagStart = null; + } + } - // <Foo checked={a < 1}> - // https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327 - if ( - token === TokenType.Unknown && - scanner.getScannerState() === ScannerState.WithinTag && - scanner.getTokenText() === '<' && - shouldBlankStartOrEndTagLike(offset) - ) { - blankStartOrEndTagLike(offset); - } + if (token === TokenType.StartTagSelfClose) { + currentStartTagStart = null; + } - token = scanner.scan(); + // <Foo checked={a < 1}> + // https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327 + if (token === TokenType.Unknown && scanner.getScannerState() === ScannerState.WithinTag && scanner.getTokenText() === '<' && shouldBlankStartOrEndTagLike(offset)) { + blankStartOrEndTagLike(offset); } - return text; + token = scanner.scan(); + } - function shouldBlankStartOrEndTagLike(offset: number) { - // not null rather than falsy, otherwise it won't work on first tag(0) - return ( - currentStartTagStart !== null && - isInsideExpression(text, currentStartTagStart, offset) - ); - } + return text; - function blankStartOrEndTagLike(offset: number) { - text = text.substring(0, offset) + ' ' + text.substring(offset + 1); - scanner = createScanner(text, offset, ScannerState.WithinTag); - } + function shouldBlankStartOrEndTagLike(offset: number) { + // not null rather than falsy, otherwise it won't work on first tag(0) + return currentStartTagStart !== null && isInsideExpression(text, currentStartTagStart, offset); + } + + function blankStartOrEndTagLike(offset: number) { + text = text.substring(0, offset) + ' ' + text.substring(offset + 1); + scanner = createScanner(text, offset, ScannerState.WithinTag); + } } export interface AttributeContext { - name: string; - inValue: boolean; - valueRange?: [number, number]; + name: string; + inValue: boolean; + valueRange?: [number, number]; } -export function getAttributeContextAtPosition( - document: Document, - position: Position -): AttributeContext | null { - const offset = document.offsetAt(position); - const { html } = document; - const tag = html.findNodeAt(offset); - - if (!inStartTag(offset, tag) || !tag.attributes) { - return null; - } +export function getAttributeContextAtPosition(document: Document, position: Position): AttributeContext | null { + const offset = document.offsetAt(position); + const { html } = document; + const tag = html.findNodeAt(offset); - const text = document.getText(); - const beforeStartTagEnd = - text.substring(0, tag.start) + preprocess(text.substring(tag.start, tag.startTagEnd)); - - const scanner = createScanner(beforeStartTagEnd, tag.start); - - let token = scanner.scan(); - let currentAttributeName: string | undefined; - const inTokenRange = () => - scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd(); - while (token != TokenType.EOS) { - // adopted from https://github.com/microsoft/vscode-html-languageservice/blob/2f7ae4df298ac2c299a40e9024d118f4a9dc0c68/src/services/htmlCompletion.ts#L402 - if (token === TokenType.AttributeName) { - currentAttributeName = scanner.getTokenText(); - - if (inTokenRange()) { - return { - name: currentAttributeName, - inValue: false - }; - } - } else if (token === TokenType.DelimiterAssign) { - if (scanner.getTokenEnd() === offset && currentAttributeName) { - const nextToken = scanner.scan(); - - return { - name: currentAttributeName, - inValue: true, - valueRange: [ - offset, - nextToken === TokenType.AttributeValue ? scanner.getTokenEnd() : offset - ] - }; - } - } else if (token === TokenType.AttributeValue) { - if (inTokenRange() && currentAttributeName) { - let start = scanner.getTokenOffset(); - let end = scanner.getTokenEnd(); - const char = text[start]; - - if (char === '"' || char === "'") { - start++; - end--; - } - - return { - name: currentAttributeName, - inValue: true, - valueRange: [start, end] - }; - } - currentAttributeName = undefined; + if (!inStartTag(offset, tag) || !tag.attributes) { + return null; + } + + const text = document.getText(); + const beforeStartTagEnd = text.substring(0, tag.start) + preprocess(text.substring(tag.start, tag.startTagEnd)); + + const scanner = createScanner(beforeStartTagEnd, tag.start); + + let token = scanner.scan(); + let currentAttributeName: string | undefined; + const inTokenRange = () => scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd(); + while (token != TokenType.EOS) { + // adopted from https://github.com/microsoft/vscode-html-languageservice/blob/2f7ae4df298ac2c299a40e9024d118f4a9dc0c68/src/services/htmlCompletion.ts#L402 + if (token === TokenType.AttributeName) { + currentAttributeName = scanner.getTokenText(); + + if (inTokenRange()) { + return { + name: currentAttributeName, + inValue: false, + }; + } + } else if (token === TokenType.DelimiterAssign) { + if (scanner.getTokenEnd() === offset && currentAttributeName) { + const nextToken = scanner.scan(); + + return { + name: currentAttributeName, + inValue: true, + valueRange: [offset, nextToken === TokenType.AttributeValue ? scanner.getTokenEnd() : offset], + }; + } + } else if (token === TokenType.AttributeValue) { + if (inTokenRange() && currentAttributeName) { + let start = scanner.getTokenOffset(); + let end = scanner.getTokenEnd(); + const char = text[start]; + + if (char === '"' || char === "'") { + start++; + end--; } - token = scanner.scan(); + + return { + name: currentAttributeName, + inValue: true, + valueRange: [start, end], + }; + } + currentAttributeName = undefined; } + token = scanner.scan(); + } - return null; + return null; } function inStartTag(offset: number, node: Node) { - return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd; + return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd; } diff --git a/tools/vscode/packages/server/src/core/documents/utils.ts b/tools/vscode/packages/server/src/core/documents/utils.ts index 6c69014d5..3d12f35a3 100644 --- a/tools/vscode/packages/server/src/core/documents/utils.ts +++ b/tools/vscode/packages/server/src/core/documents/utils.ts @@ -5,63 +5,52 @@ import { clamp } from '../../utils'; * Gets word range at position. * Delimiter is by default a whitespace, but can be adjusted. */ -export function getWordRangeAt( - str: string, - pos: number, - delimiterRegex = { left: /\S+$/, right: /\s/ } -): { start: number; end: number } { - let start = str.slice(0, pos).search(delimiterRegex.left); - if (start < 0) { - start = pos; - } - - let end = str.slice(pos).search(delimiterRegex.right); - if (end < 0) { - end = str.length; - } else { - end = end + pos; - } - - return { start, end }; +export function getWordRangeAt(str: string, pos: number, delimiterRegex = { left: /\S+$/, right: /\s/ }): { start: number; end: number } { + let start = str.slice(0, pos).search(delimiterRegex.left); + if (start < 0) { + start = pos; + } + + let end = str.slice(pos).search(delimiterRegex.right); + if (end < 0) { + end = str.length; + } else { + end = end + pos; + } + + return { start, end }; } /** * Gets word at position. * Delimiter is by default a whitespace, but can be adjusted. */ -export function getWordAt( - str: string, - pos: number, - delimiterRegex = { left: /\S+$/, right: /\s/ } -): string { - const { start, end } = getWordRangeAt(str, pos, delimiterRegex); - return str.slice(start, end); +export function getWordAt(str: string, pos: number, delimiterRegex = { left: /\S+$/, right: /\s/ }): string { + const { start, end } = getWordRangeAt(str, pos, delimiterRegex); + return str.slice(start, end); } /** * Gets index of first-non-whitespace character. */ export function getFirstNonWhitespaceIndex(str: string): number { - return str.length - str.trimStart().length; + return str.length - str.trimStart().length; } /** checks if a position is currently inside of an expression */ export function isInsideExpression(html: string, tagStart: number, position: number) { - const charactersInNode = html.substring(tagStart, position); - return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}'); + const charactersInNode = html.substring(tagStart, position); + return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}'); } /** - * Returns if a given offset is inside of the document frontmatter + * Returns if a given offset is inside of the document frontmatter */ -export function isInsideFrontmatter( - text: string, - offset: number -): boolean { - let start = text.slice(0, offset).trim().split('---').length; - let end = text.slice(offset).trim().split('---').length; - - return start > 1 && start < 3 && end >= 1; +export function isInsideFrontmatter(text: string, offset: number): boolean { + let start = text.slice(0, offset).trim().split('---').length; + let end = text.slice(offset).trim().split('---').length; + + return start > 1 && start < 3 && end >= 1; } /** @@ -70,28 +59,28 @@ export function isInsideFrontmatter( * @param text The text for which the position should be retrived */ export function positionAt(offset: number, text: string): Position { - offset = clamp(offset, 0, text.length); - - const lineOffsets = getLineOffsets(text); - 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; - } + offset = clamp(offset, 0, text.length); + + const lineOffsets = getLineOffsets(text); + 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 + // or array.length if no line offset is larger than the current offset + const line = low - 1; + return Position.create(line, offset - lineOffsets[line]); } /** @@ -100,40 +89,39 @@ export function positionAt(offset: number, text: string): Position { * @param text The text for which the offset should be retrived */ export function offsetAt(position: Position, text: string): number { - const lineOffsets = getLineOffsets(text); + const lineOffsets = getLineOffsets(text); - if (position.line >= lineOffsets.length) { - return text.length; - } else if (position.line < 0) { - return 0; - } + if (position.line >= lineOffsets.length) { + return text.length; + } else if (position.line < 0) { + return 0; + } - const lineOffset = lineOffsets[position.line]; - const nextLineOffset = - position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length; + const lineOffset = lineOffsets[position.line]; + const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length; - return clamp(nextLineOffset, lineOffset, lineOffset + position.character); + return clamp(nextLineOffset, lineOffset, lineOffset + position.character); } function getLineOffsets(text: string) { - const lineOffsets = []; - let isLineStart = true; - - for (let i = 0; i < text.length; i++) { - if (isLineStart) { - lineOffsets.push(i); - isLineStart = false; - } - const ch = text.charAt(i); - isLineStart = ch === '\r' || ch === '\n'; - if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { - i++; - } - } + const lineOffsets = []; + let isLineStart = true; - if (isLineStart && text.length > 0) { - lineOffsets.push(text.length); + for (let i = 0; i < text.length; i++) { + if (isLineStart) { + lineOffsets.push(i); + isLineStart = false; + } + const ch = text.charAt(i); + isLineStart = ch === '\r' || ch === '\n'; + if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { + i++; } + } + + if (isLineStart && text.length > 0) { + lineOffsets.push(text.length); + } - return lineOffsets; + return lineOffsets; } diff --git a/tools/vscode/packages/server/src/index.ts b/tools/vscode/packages/server/src/index.ts index f72ad550b..528d3cb9d 100644 --- a/tools/vscode/packages/server/src/index.ts +++ b/tools/vscode/packages/server/src/index.ts @@ -71,10 +71,12 @@ export function startServer() { connection.onDidChangeTextDocument((evt) => docManager.updateDocument(evt.textDocument.uri, evt.contentChanges)); connection.onDidChangeWatchedFiles((evt) => { - const params = evt.changes.map(change => ({ - fileName: urlToPath(change.uri), - changeType: change.type - })).filter(change => !!change.fileName) + const params = evt.changes + .map((change) => ({ + fileName: urlToPath(change.uri), + changeType: change.type, + })) + .filter((change) => !!change.fileName); pluginHost.onWatchFileChanges(params); }); diff --git a/tools/vscode/packages/server/src/plugins/PluginHost.ts b/tools/vscode/packages/server/src/plugins/PluginHost.ts index 72f098ca1..037dd6e07 100644 --- a/tools/vscode/packages/server/src/plugins/PluginHost.ts +++ b/tools/vscode/packages/server/src/plugins/PluginHost.ts @@ -1,11 +1,4 @@ - -import { - CompletionContext, - CompletionItem, - CompletionList, - Position, - TextDocumentIdentifier, -} from 'vscode-languageserver'; +import { CompletionContext, CompletionItem, CompletionList, Position, TextDocumentIdentifier } from 'vscode-languageserver'; import type { DocumentManager } from '../core/documents'; import type * as d from './interfaces'; import { flatten } from '../utils'; @@ -13,154 +6,107 @@ import { FoldingRange } from 'vscode-languageserver-types'; // eslint-disable-next-line no-shadow enum ExecuteMode { - None, - FirstNonNull, - Collect + None, + FirstNonNull, + Collect, } export class PluginHost { - private plugins: d.Plugin[] = []; - - constructor(private documentsManager: DocumentManager) {} + private plugins: d.Plugin[] = []; - register(plugin: d.Plugin) { - this.plugins.push(plugin); - } + constructor(private documentsManager: DocumentManager) {} - async getCompletions( - textDocument: TextDocumentIdentifier, - position: Position, - completionContext?: CompletionContext - ): Promise<CompletionList> { - const document = this.getDocument(textDocument.uri); - if (!document) { - throw new Error('Cannot call methods on an unopened document'); - } + register(plugin: d.Plugin) { + this.plugins.push(plugin); + } - const completions = ( - await this.execute<CompletionList>( - 'getCompletions', - [document, position, completionContext], - ExecuteMode.Collect - ) - ).filter((completion) => completion != null); - - let flattenedCompletions = flatten(completions.map((completion) => completion.items)); - const isIncomplete = completions.reduce( - (incomplete, completion) => incomplete || completion.isIncomplete, - false as boolean - ); - - return CompletionList.create(flattenedCompletions, isIncomplete); + async getCompletions(textDocument: TextDocumentIdentifier, position: Position, completionContext?: CompletionContext): Promise<CompletionList> { + const document = this.getDocument(textDocument.uri); + if (!document) { + throw new Error('Cannot call methods on an unopened document'); } - async resolveCompletion( - textDocument: TextDocumentIdentifier, - completionItem: d.AppCompletionItem - ): Promise<CompletionItem> { - const document = this.getDocument(textDocument.uri); + const completions = (await this.execute<CompletionList>('getCompletions', [document, position, completionContext], ExecuteMode.Collect)).filter( + (completion) => completion != null + ); - if (!document) { - throw new Error('Cannot call methods on an unopened document'); - } - - const result = await this.execute<CompletionItem>( - 'resolveCompletion', - [document, completionItem], - ExecuteMode.FirstNonNull - ); + let flattenedCompletions = flatten(completions.map((completion) => completion.items)); + const isIncomplete = completions.reduce((incomplete, completion) => incomplete || completion.isIncomplete, false as boolean); - return result ?? completionItem; - } + return CompletionList.create(flattenedCompletions, isIncomplete); + } - async doTagComplete( - textDocument: TextDocumentIdentifier, - position: Position - ): Promise<string | null> { - const document = this.getDocument(textDocument.uri); - if (!document) { - throw new Error('Cannot call methods on an unopened document'); - } + async resolveCompletion(textDocument: TextDocumentIdentifier, completionItem: d.AppCompletionItem): Promise<CompletionItem> { + const document = this.getDocument(textDocument.uri); - return this.execute<string | null>( - 'doTagComplete', - [document, position], - ExecuteMode.FirstNonNull - ); + if (!document) { + throw new Error('Cannot call methods on an unopened document'); } - async getFoldingRanges( - textDocument: TextDocumentIdentifier - ): Promise<FoldingRange[]|null> { - const document = this.getDocument(textDocument.uri); - if (!document) { - throw new Error('Cannot call methods on an unopened document'); - } + const result = await this.execute<CompletionItem>('resolveCompletion', [document, completionItem], ExecuteMode.FirstNonNull); - const foldingRanges = flatten(await this.execute<FoldingRange[]>( - 'getFoldingRanges', - [document], - ExecuteMode.Collect - )).filter((completion) => completion != null) + return result ?? completionItem; + } - return foldingRanges; + async doTagComplete(textDocument: TextDocumentIdentifier, position: Position): Promise<string | null> { + const document = this.getDocument(textDocument.uri); + if (!document) { + throw new Error('Cannot call methods on an unopened document'); } - onWatchFileChanges(onWatchFileChangesParams: any[]): void { - for (const support of this.plugins) { - support.onWatchFileChanges?.(onWatchFileChangesParams); - } - } + return this.execute<string | null>('doTagComplete', [document, position], ExecuteMode.FirstNonNull); + } - private getDocument(uri: string) { - return this.documentsManager.get(uri); + async getFoldingRanges(textDocument: TextDocumentIdentifier): Promise<FoldingRange[] | null> { + const document = this.getDocument(textDocument.uri); + if (!document) { + throw new Error('Cannot call methods on an unopened document'); } - private execute<T>( - name: keyof d.LSProvider, - args: any[], - mode: ExecuteMode.FirstNonNull - ): Promise<T | null>; - private execute<T>( - name: keyof d.LSProvider, - args: any[], - mode: ExecuteMode.Collect - ): Promise<T[]>; - private execute(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.None): Promise<void>; - private async execute<T>( - name: keyof d.LSProvider, - args: any[], - mode: ExecuteMode - ): Promise<(T | null) | T[] | void> { - const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function'); - - switch (mode) { - case ExecuteMode.FirstNonNull: - for (const plugin of plugins) { - const res = await this.tryExecutePlugin(plugin, name, args, null); - if (res != null) { - return res; - } - } - return null; - case ExecuteMode.Collect: - return Promise.all( - plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, [])) - ); - case ExecuteMode.None: - await Promise.all( - plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null)) - ); - return; - } - } + const foldingRanges = flatten(await this.execute<FoldingRange[]>('getFoldingRanges', [document], ExecuteMode.Collect)).filter((completion) => completion != null); + + return foldingRanges; + } - private async tryExecutePlugin(plugin: any, fnName: string, args: any[], failValue: any) { - try { - return await plugin[fnName](...args); - } catch (e) { - console.error(e); - return failValue; + onWatchFileChanges(onWatchFileChangesParams: any[]): void { + for (const support of this.plugins) { + support.onWatchFileChanges?.(onWatchFileChangesParams); + } + } + + private getDocument(uri: string) { + return this.documentsManager.get(uri); + } + + private execute<T>(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.FirstNonNull): Promise<T | null>; + private execute<T>(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.Collect): Promise<T[]>; + private execute(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.None): Promise<void>; + private async execute<T>(name: keyof d.LSProvider, args: any[], mode: ExecuteMode): Promise<(T | null) | T[] | void> { + const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function'); + + switch (mode) { + case ExecuteMode.FirstNonNull: + for (const plugin of plugins) { + const res = await this.tryExecutePlugin(plugin, name, args, null); + if (res != null) { + return res; + } } + return null; + case ExecuteMode.Collect: + return Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, []))); + case ExecuteMode.None: + await Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null))); + return; + } + } + + private async tryExecutePlugin(plugin: any, fnName: string, args: any[], failValue: any) { + try { + return await plugin[fnName](...args); + } catch (e) { + console.error(e); + return failValue; } + } } diff --git a/tools/vscode/packages/server/src/plugins/astro/AstroPlugin.ts b/tools/vscode/packages/server/src/plugins/astro/AstroPlugin.ts index 0696504fc..6baf407a5 100644 --- a/tools/vscode/packages/server/src/plugins/astro/AstroPlugin.ts +++ b/tools/vscode/packages/server/src/plugins/astro/AstroPlugin.ts @@ -49,7 +49,7 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider { endLine: end.line, endCharacter: end.character, kind: FoldingRangeKind.Imports, - } + }, ]; } diff --git a/tools/vscode/packages/server/src/plugins/html/HTMLPlugin.ts b/tools/vscode/packages/server/src/plugins/html/HTMLPlugin.ts index 5114eda1c..7e0ab4861 100644 --- a/tools/vscode/packages/server/src/plugins/html/HTMLPlugin.ts +++ b/tools/vscode/packages/server/src/plugins/html/HTMLPlugin.ts @@ -34,15 +34,7 @@ export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider { isIncomplete: true, items: [], }; - this.lang.setCompletionParticipants([ - getEmmetCompletionParticipants( - document, - position, - 'html', - this.configManager.getEmmetConfig(), - emmetResults - ) - ]); + this.lang.setCompletionParticipants([getEmmetCompletionParticipants(document, position, 'html', this.configManager.getEmmetConfig(), emmetResults)]); const results = this.lang.doComplete(document, position, html); const items = this.toCompletionItems(results.items); @@ -54,14 +46,13 @@ export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider { ); } - getFoldingRanges(document: Document): FoldingRange[]|null { + getFoldingRanges(document: Document): FoldingRange[] | null { const html = this.documents.get(document); if (!html) { return null; } return this.lang.getFoldingRanges(document); - } doTagComplete(document: Document, position: Position): string | null { diff --git a/tools/vscode/packages/server/src/plugins/interfaces.ts b/tools/vscode/packages/server/src/plugins/interfaces.ts index 31aafdc3e..b68100de1 100644 --- a/tools/vscode/packages/server/src/plugins/interfaces.ts +++ b/tools/vscode/packages/server/src/plugins/interfaces.ts @@ -1,217 +1,167 @@ +import { CompletionContext, FileChangeType, LinkedEditingRanges, SemanticTokens, SignatureHelpContext, TextDocumentContentChangeEvent } from 'vscode-languageserver'; import { - CompletionContext, - FileChangeType, - LinkedEditingRanges, - SemanticTokens, - SignatureHelpContext, - TextDocumentContentChangeEvent -} from 'vscode-languageserver'; -import { - CodeAction, - CodeActionContext, - Color, - ColorInformation, - ColorPresentation, - CompletionItem, - CompletionList, - DefinitionLink, - Diagnostic, - FormattingOptions, - Hover, - Location, - Position, - Range, - ReferenceContext, - SymbolInformation, - TextDocumentIdentifier, - TextEdit, - WorkspaceEdit, - SelectionRange, - SignatureHelp, - FoldingRange + CodeAction, + CodeActionContext, + Color, + ColorInformation, + ColorPresentation, + CompletionItem, + CompletionList, + DefinitionLink, + Diagnostic, + FormattingOptions, + Hover, + Location, + Position, + Range, + ReferenceContext, + SymbolInformation, + TextDocumentIdentifier, + TextEdit, + WorkspaceEdit, + SelectionRange, + SignatureHelp, + FoldingRange, } from 'vscode-languageserver-types'; import { Document } from '../core/documents'; export type Resolvable<T> = T | Promise<T>; export interface AppCompletionItem<T extends TextDocumentIdentifier = any> extends CompletionItem { - data?: T; + data?: T; } export interface AppCompletionList<T extends TextDocumentIdentifier = any> extends CompletionList { - items: Array<AppCompletionItem<T>>; + items: Array<AppCompletionItem<T>>; } export interface DiagnosticsProvider { - getDiagnostics(document: Document): Resolvable<Diagnostic[]>; + getDiagnostics(document: Document): Resolvable<Diagnostic[]>; } export interface HoverProvider { - doHover(document: Document, position: Position): Resolvable<Hover | null>; + doHover(document: Document, position: Position): Resolvable<Hover | null>; } export interface FoldingRangeProvider { - getFoldingRanges(document: Document): Resolvable<FoldingRange[]|null>; + getFoldingRanges(document: Document): Resolvable<FoldingRange[] | null>; } export interface CompletionsProvider<T extends TextDocumentIdentifier = any> { - getCompletions( - document: Document, - position: Position, - completionContext?: CompletionContext - ): Resolvable<AppCompletionList<T> | null>; + getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Resolvable<AppCompletionList<T> | null>; - resolveCompletion?( - document: Document, - completionItem: AppCompletionItem<T> - ): Resolvable<AppCompletionItem<T>>; + resolveCompletion?(document: Document, completionItem: AppCompletionItem<T>): Resolvable<AppCompletionItem<T>>; } export interface FormattingProvider { - formatDocument(document: Document, options: FormattingOptions): Resolvable<TextEdit[]>; + formatDocument(document: Document, options: FormattingOptions): Resolvable<TextEdit[]>; } export interface TagCompleteProvider { - doTagComplete(document: Document, position: Position): Resolvable<string | null>; + doTagComplete(document: Document, position: Position): Resolvable<string | null>; } export interface DocumentColorsProvider { - getDocumentColors(document: Document): Resolvable<ColorInformation[]>; + getDocumentColors(document: Document): Resolvable<ColorInformation[]>; } export interface ColorPresentationsProvider { - getColorPresentations( - document: Document, - range: Range, - color: Color - ): Resolvable<ColorPresentation[]>; + getColorPresentations(document: Document, range: Range, color: Color): Resolvable<ColorPresentation[]>; } export interface DocumentSymbolsProvider { - getDocumentSymbols(document: Document): Resolvable<SymbolInformation[]>; + getDocumentSymbols(document: Document): Resolvable<SymbolInformation[]>; } export interface DefinitionsProvider { - getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[]>; + getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[]>; } export interface BackwardsCompatibleDefinitionsProvider { - getDefinitions( - document: Document, - position: Position - ): Resolvable<DefinitionLink[] | Location[]>; + getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[] | Location[]>; } export interface CodeActionsProvider { - getCodeActions( - document: Document, - range: Range, - context: CodeActionContext - ): Resolvable<CodeAction[]>; - executeCommand?( - document: Document, - command: string, - args?: any[] - ): Resolvable<WorkspaceEdit | string | null>; + getCodeActions(document: Document, range: Range, context: CodeActionContext): Resolvable<CodeAction[]>; + executeCommand?(document: Document, command: string, args?: any[]): Resolvable<WorkspaceEdit | string | null>; } export interface FileRename { - oldUri: string; - newUri: string; + oldUri: string; + newUri: string; } export interface UpdateImportsProvider { - updateImports(fileRename: FileRename): Resolvable<WorkspaceEdit | null>; + updateImports(fileRename: FileRename): Resolvable<WorkspaceEdit | null>; } export interface RenameProvider { - rename( - document: Document, - position: Position, - newName: string - ): Resolvable<WorkspaceEdit | null>; - prepareRename(document: Document, position: Position): Resolvable<Range | null>; + rename(document: Document, position: Position, newName: string): Resolvable<WorkspaceEdit | null>; + prepareRename(document: Document, position: Position): Resolvable<Range | null>; } export interface FindReferencesProvider { - findReferences( - document: Document, - position: Position, - context: ReferenceContext - ): Promise<Location[] | null>; + findReferences(document: Document, position: Position, context: ReferenceContext): Promise<Location[] | null>; } export interface SignatureHelpProvider { - getSignatureHelp( - document: Document, - position: Position, - context: SignatureHelpContext | undefined - ): Resolvable<SignatureHelp | null>; + getSignatureHelp(document: Document, position: Position, context: SignatureHelpContext | undefined): Resolvable<SignatureHelp | null>; } export interface SelectionRangeProvider { - getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>; + getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>; } export interface SemanticTokensProvider { - getSemanticTokens(textDocument: Document, range?: Range): Resolvable<SemanticTokens | null>; + getSemanticTokens(textDocument: Document, range?: Range): Resolvable<SemanticTokens | null>; } export interface LinkedEditingRangesProvider { - getLinkedEditingRanges( - document: Document, - position: Position - ): Resolvable<LinkedEditingRanges | null>; + getLinkedEditingRanges(document: Document, position: Position): Resolvable<LinkedEditingRanges | null>; } export interface OnWatchFileChangesPara { - fileName: string; - changeType: FileChangeType; + fileName: string; + changeType: FileChangeType; } export interface OnWatchFileChanges { - onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void; + onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void; } export interface UpdateTsOrJsFile { - updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void; + updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void; } type ProviderBase = DiagnosticsProvider & - HoverProvider & - CompletionsProvider & - FormattingProvider & - FoldingRangeProvider & - TagCompleteProvider & - DocumentColorsProvider & - ColorPresentationsProvider & - DocumentSymbolsProvider & - UpdateImportsProvider & - CodeActionsProvider & - FindReferencesProvider & - RenameProvider & - SignatureHelpProvider & - SemanticTokensProvider & - LinkedEditingRangesProvider; + HoverProvider & + CompletionsProvider & + FormattingProvider & + FoldingRangeProvider & + TagCompleteProvider & + DocumentColorsProvider & + ColorPresentationsProvider & + DocumentSymbolsProvider & + UpdateImportsProvider & + CodeActionsProvider & + FindReferencesProvider & + RenameProvider & + SignatureHelpProvider & + SemanticTokensProvider & + LinkedEditingRangesProvider; export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider; export interface LSPProviderConfig { - /** - * Whether or not completion lists that are marked as imcomplete - * should be filtered server side. - */ - filterIncompleteCompletions: boolean; - /** - * Whether or not getDefinitions supports the LocationLink interface. - */ - definitionLinkSupport: boolean; -} - -export type Plugin = Partial< - ProviderBase & - DefinitionsProvider & - OnWatchFileChanges & - SelectionRangeProvider & - UpdateTsOrJsFile ->; + /** + * Whether or not completion lists that are marked as imcomplete + * should be filtered server side. + */ + filterIncompleteCompletions: boolean; + /** + * Whether or not getDefinitions supports the LocationLink interface. + */ + definitionLinkSupport: boolean; +} + +export type Plugin = Partial<ProviderBase & DefinitionsProvider & OnWatchFileChanges & SelectionRangeProvider & UpdateTsOrJsFile>; diff --git a/tools/vscode/packages/server/src/plugins/typescript/LanguageServiceManager.ts b/tools/vscode/packages/server/src/plugins/typescript/LanguageServiceManager.ts index 60dec606c..529ab2b4c 100644 --- a/tools/vscode/packages/server/src/plugins/typescript/LanguageServiceManager.ts +++ b/tools/vscode/packages/server/src/plugins/typescript/LanguageServiceManager.ts @@ -38,7 +38,7 @@ export class LanguageServiceManager { const url = urlToPath(curr) as string; if (fileName.startsWith(url) && curr.length < url.length) return url; return found; - }, '') + }, ''); } private createDocument = (fileName: string, content: string) => { diff --git a/tools/vscode/packages/server/src/plugins/typescript/SnapshotManager.ts b/tools/vscode/packages/server/src/plugins/typescript/SnapshotManager.ts index aac26d96e..47d44838d 100644 --- a/tools/vscode/packages/server/src/plugins/typescript/SnapshotManager.ts +++ b/tools/vscode/packages/server/src/plugins/typescript/SnapshotManager.ts @@ -6,328 +6,298 @@ import { pathToUrl } from '../../utils'; import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } from './utils'; export interface TsFilesSpec { - include?: readonly string[]; - exclude?: readonly string[]; + include?: readonly string[]; + exclude?: readonly string[]; } export class SnapshotManager { - private documents: Map<string, DocumentSnapshot> = new Map(); - private lastLogged = new Date(new Date().getTime() - 60_001); - - private readonly watchExtensions = [ - ts.Extension.Dts, - ts.Extension.Js, - ts.Extension.Jsx, - ts.Extension.Ts, - ts.Extension.Tsx, - ts.Extension.Json - ]; - - constructor( - private projectFiles: string[], - private fileSpec: TsFilesSpec, - private workspaceRoot: string - ) { - - } + private documents: Map<string, DocumentSnapshot> = new Map(); + private lastLogged = new Date(new Date().getTime() - 60_001); - updateProjectFiles() { - const { include, exclude } = this.fileSpec; - - if (include?.length === 0) return; + private readonly watchExtensions = [ts.Extension.Dts, ts.Extension.Js, ts.Extension.Jsx, ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Json]; - const projectFiles = ts.sys.readDirectory( - this.workspaceRoot, - this.watchExtensions, - exclude, - include - ); + constructor(private projectFiles: string[], private fileSpec: TsFilesSpec, private workspaceRoot: string) {} - this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles])); - } + updateProjectFiles() { + const { include, exclude } = this.fileSpec; - updateProjectFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void { - const previousSnapshot = this.get(fileName); - - if (changes) { - if (!(previousSnapshot instanceof TypeScriptDocumentSnapshot)) { - return; - } - previousSnapshot.update(changes); - } else { - const newSnapshot = createDocumentSnapshot(fileName); - - if (previousSnapshot) { - newSnapshot.version = previousSnapshot.version + 1; - } else { - // ensure it's greater than initial version - // so that ts server picks up the change - newSnapshot.version += 1; - } - this.set(fileName, newSnapshot); - } - } + if (include?.length === 0) return; - has(fileName: string) { - return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName); - } + const projectFiles = ts.sys.readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include); - get(fileName: string) { - return this.documents.get(fileName); - } - - set(fileName: string, snapshot: DocumentSnapshot) { - // const prev = this.get(fileName); - this.logStatistics(); - return this.documents.set(fileName, snapshot); - } - - delete(fileName: string) { - this.projectFiles = this.projectFiles.filter((s) => s !== fileName); - return this.documents.delete(fileName); - } + this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles])); + } - getFileNames() { - return Array.from(this.documents.keys()).map(fileName => toVirtualAstroFilePath(fileName)); - } + updateProjectFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void { + const previousSnapshot = this.get(fileName); - getProjectFileNames() { - return [...this.projectFiles]; - } - - private logStatistics() { - const date = new Date(); - // Don't use setInterval because that will keep tests running forever - if (date.getTime() - this.lastLogged.getTime() > 60_000) { - this.lastLogged = date; - - const projectFiles = this.getProjectFileNames(); - const allFiles = Array.from(new Set([...projectFiles, ...this.getFileNames()])); - console.log( - 'SnapshotManager File Statistics:\n' + - `Project files: ${projectFiles.length}\n` + - `Astro files: ${ - allFiles.filter((name) => name.endsWith('.astro')).length - }\n` + - `From node_modules: ${ - allFiles.filter((name) => name.includes('node_modules')).length - }\n` + - `Total: ${allFiles.length}` - ); - } - } + if (changes) { + if (!(previousSnapshot instanceof TypeScriptDocumentSnapshot)) { + return; + } + previousSnapshot.update(changes); + } else { + const newSnapshot = createDocumentSnapshot(fileName); + + if (previousSnapshot) { + newSnapshot.version = previousSnapshot.version + 1; + } else { + // ensure it's greater than initial version + // so that ts server picks up the change + newSnapshot.version += 1; + } + this.set(fileName, newSnapshot); + } + } + + has(fileName: string) { + return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName); + } + + get(fileName: string) { + return this.documents.get(fileName); + } + + set(fileName: string, snapshot: DocumentSnapshot) { + // const prev = this.get(fileName); + this.logStatistics(); + return this.documents.set(fileName, snapshot); + } + + delete(fileName: string) { + this.projectFiles = this.projectFiles.filter((s) => s !== fileName); + return this.documents.delete(fileName); + } + + getFileNames() { + return Array.from(this.documents.keys()).map((fileName) => toVirtualAstroFilePath(fileName)); + } + + getProjectFileNames() { + return [...this.projectFiles]; + } + + private logStatistics() { + const date = new Date(); + // Don't use setInterval because that will keep tests running forever + if (date.getTime() - this.lastLogged.getTime() > 60_000) { + this.lastLogged = date; + + const projectFiles = this.getProjectFileNames(); + const allFiles = Array.from(new Set([...projectFiles, ...this.getFileNames()])); + console.log( + 'SnapshotManager File Statistics:\n' + + `Project files: ${projectFiles.length}\n` + + `Astro files: ${allFiles.filter((name) => name.endsWith('.astro')).length}\n` + + `From node_modules: ${allFiles.filter((name) => name.includes('node_modules')).length}\n` + + `Total: ${allFiles.length}` + ); + } + } } export interface DocumentSnapshot extends ts.IScriptSnapshot { - version: number; - filePath: string; - scriptKind: ts.ScriptKind; - positionAt(offset: number): Position; - /** - * Instantiates a source mapper. - * `destroyFragment` needs to be called when - * it's no longer needed / the class should be cleaned up - * in order to prevent memory leaks. - */ - getFragment(): Promise<DocumentFragmentSnapshot>; - /** - * Needs to be called when source mapper - * is no longer needed / the class should be cleaned up - * in order to prevent memory leaks. - */ - destroyFragment(): void; - /** - * Convenience function for getText(0, getLength()) - */ - getFullText(): string; + version: number; + filePath: string; + scriptKind: ts.ScriptKind; + positionAt(offset: number): Position; + /** + * Instantiates a source mapper. + * `destroyFragment` needs to be called when + * it's no longer needed / the class should be cleaned up + * in order to prevent memory leaks. + */ + getFragment(): Promise<DocumentFragmentSnapshot>; + /** + * Needs to be called when source mapper + * is no longer needed / the class should be cleaned up + * in order to prevent memory leaks. + */ + destroyFragment(): void; + /** + * Convenience function for getText(0, getLength()) + */ + getFullText(): string; } export const createDocumentSnapshot = (filePath: string, createDocument?: (_filePath: string, text: string) => Document): DocumentSnapshot => { - const text = ts.sys.readFile(filePath) ?? ''; + const text = ts.sys.readFile(filePath) ?? ''; - if (isAstroFilePath(filePath)) { - if (!createDocument) throw new Error('Astro documents require the "createDocument" utility to be provided'); - const snapshot = new AstroDocumentSnapshot(createDocument(filePath, text)); - return snapshot; - } - - return new TypeScriptDocumentSnapshot(0, filePath, text); + if (isAstroFilePath(filePath)) { + if (!createDocument) throw new Error('Astro documents require the "createDocument" utility to be provided'); + const snapshot = new AstroDocumentSnapshot(createDocument(filePath, text)); + return snapshot; + } -} + return new TypeScriptDocumentSnapshot(0, filePath, text); +}; class AstroDocumentSnapshot implements DocumentSnapshot { - - version = this.doc.version; - scriptKind = ts.ScriptKind.Unknown; - - constructor(private doc: Document) {} - - async getFragment(): Promise<DocumentFragmentSnapshot> { - return new DocumentFragmentSnapshot(this.doc); - } + version = this.doc.version; + scriptKind = ts.ScriptKind.Unknown; - async destroyFragment() { - return; - } + constructor(private doc: Document) {} - get text() { - return this.doc.getText(); - } + async getFragment(): Promise<DocumentFragmentSnapshot> { + return new DocumentFragmentSnapshot(this.doc); + } - get filePath() { - return this.doc.getFilePath() || ''; - } + async destroyFragment() { + return; + } - getText(start: number, end: number) { - return this.text.substring(start, end); - } + get text() { + return this.doc.getText(); + } - getLength() { - return this.text.length; - } + get filePath() { + return this.doc.getFilePath() || ''; + } - getFullText() { - return this.text; - } + getText(start: number, end: number) { + return this.text.substring(start, end); + } - getChangeRange() { - return undefined; - } + getLength() { + return this.text.length; + } - positionAt(offset: number) { - return positionAt(offset, this.text); - } + getFullText() { + return this.text; + } - getLineContainingOffset(offset: number) { - const chunks = this.getText(0, offset).split('\n'); - return chunks[chunks.length - 1]; - } - - offsetAt(position: Position) { - return offsetAt(position, this.text); - } - -} + getChangeRange() { + return undefined; + } -class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFragment'|'destroyFragment'> { - - version: number; - filePath: string; - url: string; - text: string; - - scriptKind = ts.ScriptKind.TSX; - scriptInfo = null; - - constructor( - private doc: Document - ) { - const filePath = doc.getFilePath(); - if (!filePath) throw new Error('Cannot create a document fragment from a non-local document'); - const text = doc.getText(); - this.version = doc.version; - this.filePath = toVirtualAstroFilePath(filePath); - this.url = toVirtualAstroFilePath(filePath); - this.text = this.transformContent(text); - } + positionAt(offset: number) { + return positionAt(offset, this.text); + } - /** @internal */ - private transformContent(content: string) { - return content.replace(/---/g, '///'); - } + getLineContainingOffset(offset: number) { + const chunks = this.getText(0, offset).split('\n'); + return chunks[chunks.length - 1]; + } - getText(start: number, end: number) { - return this.text.substring(start, end); - } - - getLength() { - return this.text.length; - } - - getFullText() { - return this.text; - } - - getChangeRange() { - return undefined; - } - - positionAt(offset: number) { - return positionAt(offset, this.text); - } - - getLineContainingOffset(offset: number) { - const chunks = this.getText(0, offset).split('\n'); - return chunks[chunks.length - 1]; - } + offsetAt(position: Position) { + return offsetAt(position, this.text); + } +} - offsetAt(position: Position): number { - return offsetAt(position, this.text); - } +class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFragment' | 'destroyFragment'> { + version: number; + filePath: string; + url: string; + text: string; + + scriptKind = ts.ScriptKind.TSX; + scriptInfo = null; + + constructor(private doc: Document) { + const filePath = doc.getFilePath(); + if (!filePath) throw new Error('Cannot create a document fragment from a non-local document'); + const text = doc.getText(); + this.version = doc.version; + this.filePath = toVirtualAstroFilePath(filePath); + this.url = toVirtualAstroFilePath(filePath); + this.text = this.transformContent(text); + } + + /** @internal */ + private transformContent(content: string) { + return content.replace(/---/g, '///'); + } + + getText(start: number, end: number) { + return this.text.substring(start, end); + } + + getLength() { + return this.text.length; + } + + getFullText() { + return this.text; + } + + getChangeRange() { + return undefined; + } + + positionAt(offset: number) { + return positionAt(offset, this.text); + } + + getLineContainingOffset(offset: number) { + const chunks = this.getText(0, offset).split('\n'); + return chunks[chunks.length - 1]; + } + + offsetAt(position: Position): number { + return offsetAt(position, this.text); + } } class TypeScriptDocumentSnapshot implements DocumentSnapshot { - - scriptKind = getScriptKindFromFileName(this.filePath); - scriptInfo = null; - url: string; - - - constructor(public version: number, public readonly filePath: string, private text: string) { - this.url = pathToUrl(filePath) - } - - getText(start: number, end: number) { - return this.text.substring(start, end); - } - - getLength() { - return this.text.length; - } - - getFullText() { - return this.text; - } - - getChangeRange() { - return undefined; - } - - positionAt(offset: number) { - return positionAt(offset, this.text); - } - - offsetAt(position: Position): number { - return offsetAt(position, this.text); - } - - async getFragment(): Promise<DocumentFragmentSnapshot> { - return this as unknown as any; - } - - destroyFragment() { - // nothing to clean up - } - - getLineContainingOffset(offset: number) { - const chunks = this.getText(0, offset).split('\n'); - return chunks[chunks.length - 1]; - } - - update(changes: TextDocumentContentChangeEvent[]): void { - for (const change of changes) { - let start = 0; - let end = 0; - if ('range' in change) { - start = this.offsetAt(change.range.start); - end = this.offsetAt(change.range.end); - } else { - end = this.getLength(); - } - - this.text = this.text.slice(0, start) + change.text + this.text.slice(end); - } - - this.version++; - } + scriptKind = getScriptKindFromFileName(this.filePath); + scriptInfo = null; + url: string; + + constructor(public version: number, public readonly filePath: string, private text: string) { + this.url = pathToUrl(filePath); + } + + getText(start: number, end: number) { + return this.text.substring(start, end); + } + + getLength() { + return this.text.length; + } + + getFullText() { + return this.text; + } + + getChangeRange() { + return undefined; + } + + positionAt(offset: number) { + return positionAt(offset, this.text); + } + + offsetAt(position: Position): number { + return offsetAt(position, this.text); + } + + async getFragment(): Promise<DocumentFragmentSnapshot> { + return (this as unknown) as any; + } + + destroyFragment() { + // nothing to clean up + } + + getLineContainingOffset(offset: number) { + const chunks = this.getText(0, offset).split('\n'); + return chunks[chunks.length - 1]; + } + + update(changes: TextDocumentContentChangeEvent[]): void { + for (const change of changes) { + let start = 0; + let end = 0; + if ('range' in change) { + start = this.offsetAt(change.range.start); + end = this.offsetAt(change.range.end); + } else { + end = this.getLength(); + } + + this.text = this.text.slice(0, start) + change.text + this.text.slice(end); + } + + this.version++; + } } diff --git a/tools/vscode/packages/server/src/plugins/typescript/TypeScriptPlugin.ts b/tools/vscode/packages/server/src/plugins/typescript/TypeScriptPlugin.ts index 018e8bfda..aab758bdb 100644 --- a/tools/vscode/packages/server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/tools/vscode/packages/server/src/plugins/typescript/TypeScriptPlugin.ts @@ -1,11 +1,7 @@ import type { Document, DocumentManager } from '../../core/documents'; import type { ConfigManager } from '../../core/config'; import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces'; -import { - CompletionContext, - Position, - FileChangeType -} from 'vscode-languageserver'; +import { CompletionContext, Position, FileChangeType } from 'vscode-languageserver'; import * as ts from 'typescript'; import { CompletionsProviderImpl, CompletionEntryWithIdentifer } from './features/CompletionsProvider'; import { LanguageServiceManager } from './LanguageServiceManager'; @@ -13,77 +9,61 @@ import { SnapshotManager } from './SnapshotManager'; import { getScriptKindFromFileName } from './utils'; export class TypeScriptPlugin implements CompletionsProvider { - private readonly docManager: DocumentManager; - private readonly configManager: ConfigManager; - private readonly languageServiceManager: LanguageServiceManager; + private readonly docManager: DocumentManager; + private readonly configManager: ConfigManager; + private readonly languageServiceManager: LanguageServiceManager; - private readonly completionProvider: CompletionsProviderImpl; + private readonly completionProvider: CompletionsProviderImpl; - constructor( - docManager: DocumentManager, - configManager: ConfigManager, - workspaceUris: string[] - ) { - this.docManager = docManager; - this.configManager = configManager; - this.languageServiceManager = new LanguageServiceManager(docManager, configManager, workspaceUris); - - this.completionProvider = new CompletionsProviderImpl(this.languageServiceManager); - } + constructor(docManager: DocumentManager, configManager: ConfigManager, workspaceUris: string[]) { + this.docManager = docManager; + this.configManager = configManager; + this.languageServiceManager = new LanguageServiceManager(docManager, configManager, workspaceUris); - async getCompletions( - document: Document, - position: Position, - completionContext?: CompletionContext - ): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> { - const completions = await this.completionProvider.getCompletions( - document, - position, - completionContext - ); + this.completionProvider = new CompletionsProviderImpl(this.languageServiceManager); + } - return completions; - } + async getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> { + const completions = await this.completionProvider.getCompletions(document, position, completionContext); - async resolveCompletion( - document: Document, - completionItem: AppCompletionItem<CompletionEntryWithIdentifer> - ): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> { - return this.completionProvider.resolveCompletion(document, completionItem); - } + return completions; + } - async onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void> { - const doneUpdateProjectFiles = new Set<SnapshotManager>(); + async resolveCompletion(document: Document, completionItem: AppCompletionItem<CompletionEntryWithIdentifer>): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> { + return this.completionProvider.resolveCompletion(document, completionItem); + } - for (const { fileName, changeType } of onWatchFileChangesParams) { - const scriptKind = getScriptKindFromFileName(fileName); + async onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void> { + const doneUpdateProjectFiles = new Set<SnapshotManager>(); - if (scriptKind === ts.ScriptKind.Unknown) { - // We don't deal with svelte files here - continue; - } + for (const { fileName, changeType } of onWatchFileChangesParams) { + const scriptKind = getScriptKindFromFileName(fileName); - const snapshotManager = await this.getSnapshotManager(fileName); - if (changeType === FileChangeType.Created) { - if (!doneUpdateProjectFiles.has(snapshotManager)) { - snapshotManager.updateProjectFiles(); - doneUpdateProjectFiles.add(snapshotManager); - } - } else if (changeType === FileChangeType.Deleted) { - snapshotManager.delete(fileName); - return; - } + if (scriptKind === ts.ScriptKind.Unknown) { + // We don't deal with svelte files here + continue; + } - snapshotManager.updateProjectFile(fileName); + const snapshotManager = await this.getSnapshotManager(fileName); + if (changeType === FileChangeType.Created) { + if (!doneUpdateProjectFiles.has(snapshotManager)) { + snapshotManager.updateProjectFiles(); + doneUpdateProjectFiles.add(snapshotManager); } - } + } else if (changeType === FileChangeType.Deleted) { + snapshotManager.delete(fileName); + return; + } - /** - * - * @internal - */ - public async getSnapshotManager(fileName: string) { - return this.languageServiceManager.getSnapshotManager(fileName); + snapshotManager.updateProjectFile(fileName); } -} + } + /** + * + * @internal + */ + public async getSnapshotManager(fileName: string) { + return this.languageServiceManager.getSnapshotManager(fileName); + } +} diff --git a/tools/vscode/packages/server/src/plugins/typescript/astro-sys.ts b/tools/vscode/packages/server/src/plugins/typescript/astro-sys.ts index 0459528c5..36d009eb6 100644 --- a/tools/vscode/packages/server/src/plugins/typescript/astro-sys.ts +++ b/tools/vscode/packages/server/src/plugins/typescript/astro-sys.ts @@ -6,37 +6,37 @@ import { ensureRealAstroFilePath, isAstroFilePath, isVirtualAstroFilePath, toRea * This should only be accessed by TS Astro module resolution. */ export function createAstroSys(getSnapshot: (fileName: string) => DocumentSnapshot) { - const AstroSys: ts.System = { - ...ts.sys, - fileExists(path: string) { - if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) { - console.log('fileExists', path, ts.sys.fileExists(ensureRealAstroFilePath(path))); - } - return ts.sys.fileExists(ensureRealAstroFilePath(path)); - }, - readFile(path: string) { - if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) { - console.log('readFile', path); - } - const snapshot = getSnapshot(path); - return snapshot.getFullText(); - }, - readDirectory(path, extensions, exclude, include, depth) { - const extensionsWithAstro = (extensions ?? []).concat(...['.astro']); - const result = ts.sys.readDirectory(path, extensionsWithAstro, exclude, include, depth);; - return result; - } - }; + const AstroSys: ts.System = { + ...ts.sys, + fileExists(path: string) { + if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) { + console.log('fileExists', path, ts.sys.fileExists(ensureRealAstroFilePath(path))); + } + return ts.sys.fileExists(ensureRealAstroFilePath(path)); + }, + readFile(path: string) { + if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) { + console.log('readFile', path); + } + const snapshot = getSnapshot(path); + return snapshot.getFullText(); + }, + readDirectory(path, extensions, exclude, include, depth) { + const extensionsWithAstro = (extensions ?? []).concat(...['.astro']); + const result = ts.sys.readDirectory(path, extensionsWithAstro, exclude, include, depth); + return result; + }, + }; - if (ts.sys.realpath) { - const realpath = ts.sys.realpath; - AstroSys.realpath = function (path) { - if (isVirtualAstroFilePath(path)) { - return realpath(toRealAstroFilePath(path)) + '.ts'; - } - return realpath(path); - }; - } + if (ts.sys.realpath) { + const realpath = ts.sys.realpath; + AstroSys.realpath = function (path) { + if (isVirtualAstroFilePath(path)) { + return realpath(toRealAstroFilePath(path)) + '.ts'; + } + return realpath(path); + }; + } - return AstroSys; + return AstroSys; } diff --git a/tools/vscode/packages/server/src/plugins/typescript/features/CompletionsProvider.ts b/tools/vscode/packages/server/src/plugins/typescript/features/CompletionsProvider.ts index ebbc16e31..348f3e4ae 100644 --- a/tools/vscode/packages/server/src/plugins/typescript/features/CompletionsProvider.ts +++ b/tools/vscode/packages/server/src/plugins/typescript/features/CompletionsProvider.ts @@ -99,7 +99,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn data: { ...comp, uri, - position + position, }, }; } diff --git a/tools/vscode/packages/server/src/plugins/typescript/languageService.ts b/tools/vscode/packages/server/src/plugins/typescript/languageService.ts index 4de703b2a..098c335e7 100644 --- a/tools/vscode/packages/server/src/plugins/typescript/languageService.ts +++ b/tools/vscode/packages/server/src/plugins/typescript/languageService.ts @@ -53,30 +53,22 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string return ts.sys.readDirectory(path, [...extensions, '.vue', '.svelte', '.astro', '.js', '.jsx'], exclude, include, depth); }, }; - + let configJson = (tsconfigPath && ts.readConfigFile(tsconfigPath, ts.sys.readFile).config) || getDefaultJsConfig(); if (!configJson.extends) { - configJson = Object.assign( - { - exclude: getDefaultExclude() - }, - configJson - ); + configJson = Object.assign( + { + exclude: getDefaultExclude(), + }, + configJson + ); } - const project = ts.parseJsonConfigFileContent( - configJson, - parseConfigHost, - workspaceRoot, - {}, - basename(tsconfigPath), - undefined, - [ - { extension: '.vue', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }, - { extension: '.svelte', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }, - { extension: '.astro', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred } - ] - ); + const project = ts.parseJsonConfigFileContent(configJson, parseConfigHost, workspaceRoot, {}, basename(tsconfigPath), undefined, [ + { extension: '.vue', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }, + { extension: '.svelte', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }, + { extension: '.astro', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }, + ]); let projectVersion = 0; const snapshotManager = new SnapshotManager(project.fileNames, { exclude: ['node_modules', 'dist'], include: ['astro'] }, workspaceRoot || process.cwd()); @@ -100,15 +92,15 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string getProjectVersion: () => `${projectVersion}`, getScriptFileNames: () => Array.from(new Set([...snapshotManager.getFileNames(), ...snapshotManager.getProjectFileNames()])), getScriptSnapshot, - getScriptVersion: (fileName: string) => getScriptSnapshot(fileName).version.toString() + getScriptVersion: (fileName: string) => getScriptSnapshot(fileName).version.toString(), }; const languageService = ts.createLanguageService(host); const languageServiceProxy = new Proxy(languageService, { get(target, prop) { return Reflect.get(target, prop); - } - }) + }, + }); return { tsconfigPath, @@ -141,19 +133,16 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string } function getScriptSnapshot(fileName: string): DocumentSnapshot { - fileName = ensureRealAstroFilePath(fileName); - - let doc = snapshotManager.get(fileName); - if (doc) { - return doc; - } - - doc = createDocumentSnapshot( - fileName, - docContext.createDocument, - ); - snapshotManager.set(fileName, doc); + fileName = ensureRealAstroFilePath(fileName); + + let doc = snapshotManager.get(fileName); + if (doc) { return doc; + } + + doc = createDocumentSnapshot(fileName, docContext.createDocument); + snapshotManager.set(fileName, doc); + return doc; } } @@ -168,7 +157,7 @@ function getDefaultJsConfig(): { compilerOptions: { maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, - allowJs: true + allowJs: true, }, include: ['astro'], }; diff --git a/tools/vscode/packages/server/src/plugins/typescript/utils.ts b/tools/vscode/packages/server/src/plugins/typescript/utils.ts index 058868474..1f42e7d0a 100644 --- a/tools/vscode/packages/server/src/plugins/typescript/utils.ts +++ b/tools/vscode/packages/server/src/plugins/typescript/utils.ts @@ -3,180 +3,172 @@ import { CompletionItemKind, DiagnosticSeverity } from 'vscode-languageserver'; import { dirname } from 'path'; import { pathToUrl } from '../../utils'; -export function scriptElementKindToCompletionItemKind( - kind: ts.ScriptElementKind -): CompletionItemKind { - switch (kind) { - case ts.ScriptElementKind.primitiveType: - case ts.ScriptElementKind.keyword: - return CompletionItemKind.Keyword; - case ts.ScriptElementKind.constElement: - return CompletionItemKind.Constant; - case ts.ScriptElementKind.letElement: - case ts.ScriptElementKind.variableElement: - case ts.ScriptElementKind.localVariableElement: - case ts.ScriptElementKind.alias: - return CompletionItemKind.Variable; - case ts.ScriptElementKind.memberVariableElement: - case ts.ScriptElementKind.memberGetAccessorElement: - case ts.ScriptElementKind.memberSetAccessorElement: - return CompletionItemKind.Field; - case ts.ScriptElementKind.functionElement: - return CompletionItemKind.Function; - case ts.ScriptElementKind.memberFunctionElement: - case ts.ScriptElementKind.constructSignatureElement: - case ts.ScriptElementKind.callSignatureElement: - case ts.ScriptElementKind.indexSignatureElement: - return CompletionItemKind.Method; - case ts.ScriptElementKind.enumElement: - return CompletionItemKind.Enum; - case ts.ScriptElementKind.moduleElement: - case ts.ScriptElementKind.externalModuleName: - return CompletionItemKind.Module; - case ts.ScriptElementKind.classElement: - case ts.ScriptElementKind.typeElement: - return CompletionItemKind.Class; - case ts.ScriptElementKind.interfaceElement: - return CompletionItemKind.Interface; - case ts.ScriptElementKind.warning: - case ts.ScriptElementKind.scriptElement: - return CompletionItemKind.File; - case ts.ScriptElementKind.directory: - return CompletionItemKind.Folder; - case ts.ScriptElementKind.string: - return CompletionItemKind.Constant; - } - return CompletionItemKind.Property; +export function scriptElementKindToCompletionItemKind(kind: ts.ScriptElementKind): CompletionItemKind { + switch (kind) { + case ts.ScriptElementKind.primitiveType: + case ts.ScriptElementKind.keyword: + return CompletionItemKind.Keyword; + case ts.ScriptElementKind.constElement: + return CompletionItemKind.Constant; + case ts.ScriptElementKind.letElement: + case ts.ScriptElementKind.variableElement: + case ts.ScriptElementKind.localVariableElement: + case ts.ScriptElementKind.alias: + return CompletionItemKind.Variable; + case ts.ScriptElementKind.memberVariableElement: + case ts.ScriptElementKind.memberGetAccessorElement: + case ts.ScriptElementKind.memberSetAccessorElement: + return CompletionItemKind.Field; + case ts.ScriptElementKind.functionElement: + return CompletionItemKind.Function; + case ts.ScriptElementKind.memberFunctionElement: + case ts.ScriptElementKind.constructSignatureElement: + case ts.ScriptElementKind.callSignatureElement: + case ts.ScriptElementKind.indexSignatureElement: + return CompletionItemKind.Method; + case ts.ScriptElementKind.enumElement: + return CompletionItemKind.Enum; + case ts.ScriptElementKind.moduleElement: + case ts.ScriptElementKind.externalModuleName: + return CompletionItemKind.Module; + case ts.ScriptElementKind.classElement: + case ts.ScriptElementKind.typeElement: + return CompletionItemKind.Class; + case ts.ScriptElementKind.interfaceElement: + return CompletionItemKind.Interface; + case ts.ScriptElementKind.warning: + case ts.ScriptElementKind.scriptElement: + return CompletionItemKind.File; + case ts.ScriptElementKind.directory: + return CompletionItemKind.Folder; + case ts.ScriptElementKind.string: + return CompletionItemKind.Constant; + } + return CompletionItemKind.Property; } -export function getCommitCharactersForScriptElement( - kind: ts.ScriptElementKind -): string[] | undefined { - const commitCharacters: string[] = []; - switch (kind) { - case ts.ScriptElementKind.memberGetAccessorElement: - case ts.ScriptElementKind.memberSetAccessorElement: - case ts.ScriptElementKind.constructSignatureElement: - case ts.ScriptElementKind.callSignatureElement: - case ts.ScriptElementKind.indexSignatureElement: - case ts.ScriptElementKind.enumElement: - case ts.ScriptElementKind.interfaceElement: - commitCharacters.push('.'); - break; - - case ts.ScriptElementKind.moduleElement: - case ts.ScriptElementKind.alias: - case ts.ScriptElementKind.constElement: - case ts.ScriptElementKind.letElement: - case ts.ScriptElementKind.variableElement: - case ts.ScriptElementKind.localVariableElement: - case ts.ScriptElementKind.memberVariableElement: - case ts.ScriptElementKind.classElement: - case ts.ScriptElementKind.functionElement: - case ts.ScriptElementKind.memberFunctionElement: - commitCharacters.push('.', ','); - commitCharacters.push('('); - break; - } - - return commitCharacters.length === 0 ? undefined : commitCharacters; +export function getCommitCharactersForScriptElement(kind: ts.ScriptElementKind): string[] | undefined { + const commitCharacters: string[] = []; + switch (kind) { + case ts.ScriptElementKind.memberGetAccessorElement: + case ts.ScriptElementKind.memberSetAccessorElement: + case ts.ScriptElementKind.constructSignatureElement: + case ts.ScriptElementKind.callSignatureElement: + case ts.ScriptElementKind.indexSignatureElement: + case ts.ScriptElementKind.enumElement: + case ts.ScriptElementKind.interfaceElement: + commitCharacters.push('.'); + break; + + case ts.ScriptElementKind.moduleElement: + case ts.ScriptElementKind.alias: + case ts.ScriptElementKind.constElement: + case ts.ScriptElementKind.letElement: + case ts.ScriptElementKind.variableElement: + case ts.ScriptElementKind.localVariableElement: + case ts.ScriptElementKind.memberVariableElement: + case ts.ScriptElementKind.classElement: + case ts.ScriptElementKind.functionElement: + case ts.ScriptElementKind.memberFunctionElement: + commitCharacters.push('.', ','); + commitCharacters.push('('); + break; + } + + return commitCharacters.length === 0 ? undefined : commitCharacters; } export function mapSeverity(category: ts.DiagnosticCategory): DiagnosticSeverity { - switch (category) { - case ts.DiagnosticCategory.Error: - return DiagnosticSeverity.Error; - case ts.DiagnosticCategory.Warning: - return DiagnosticSeverity.Warning; - case ts.DiagnosticCategory.Suggestion: - return DiagnosticSeverity.Hint; - case ts.DiagnosticCategory.Message: - return DiagnosticSeverity.Information; - } - - return DiagnosticSeverity.Error; + switch (category) { + case ts.DiagnosticCategory.Error: + return DiagnosticSeverity.Error; + case ts.DiagnosticCategory.Warning: + return DiagnosticSeverity.Warning; + case ts.DiagnosticCategory.Suggestion: + return DiagnosticSeverity.Hint; + case ts.DiagnosticCategory.Message: + return DiagnosticSeverity.Information; + } + + return DiagnosticSeverity.Error; } export function getScriptKindFromFileName(fileName: string): ts.ScriptKind { - const ext = fileName.substr(fileName.lastIndexOf('.')); - switch (ext.toLowerCase()) { - case ts.Extension.Js: - return ts.ScriptKind.JS; - case ts.Extension.Jsx: - return ts.ScriptKind.JSX; - case ts.Extension.Ts: - return ts.ScriptKind.TS; - case ts.Extension.Tsx: - return ts.ScriptKind.TSX; - case ts.Extension.Json: - return ts.ScriptKind.JSON; - default: - return ts.ScriptKind.Unknown; - } + const ext = fileName.substr(fileName.lastIndexOf('.')); + switch (ext.toLowerCase()) { + case ts.Extension.Js: + return ts.ScriptKind.JS; + case ts.Extension.Jsx: + return ts.ScriptKind.JSX; + case ts.Extension.Ts: + return ts.ScriptKind.TS; + case ts.Extension.Tsx: + return ts.ScriptKind.TSX; + case ts.Extension.Json: + return ts.ScriptKind.JSON; + default: + return ts.ScriptKind.Unknown; + } } export function isAstroFilePath(filePath: string) { - return filePath.endsWith('.astro'); + return filePath.endsWith('.astro'); } export function isVirtualAstroFilePath(filePath: string) { - return filePath.endsWith('.astro.ts'); + return filePath.endsWith('.astro.ts'); } export function toVirtualAstroFilePath(filePath: string) { - return `${filePath}.ts`; + return `${filePath}.ts`; } export function toRealAstroFilePath(filePath: string) { - return filePath.slice(0, -'.ts'.length); + return filePath.slice(0, -'.ts'.length); } export function ensureRealAstroFilePath(filePath: string) { - return isVirtualAstroFilePath(filePath) ? toRealAstroFilePath(filePath) : filePath; + return isVirtualAstroFilePath(filePath) ? toRealAstroFilePath(filePath) : filePath; } export function findTsConfigPath(fileName: string, rootUris: string[]) { - const searchDir = dirname(fileName); - const path = - ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') || - ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') || - ''; - // Don't return config files that exceed the current workspace context. - return !!path && rootUris.some((rootUri) => isSubPath(rootUri, path)) ? path : ''; + const searchDir = dirname(fileName); + const path = ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') || ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') || ''; + // Don't return config files that exceed the current workspace context. + return !!path && rootUris.some((rootUri) => isSubPath(rootUri, path)) ? path : ''; } /** */ export function isSubPath(uri: string, possibleSubPath: string): boolean { - return pathToUrl(possibleSubPath).startsWith(uri); + return pathToUrl(possibleSubPath).startsWith(uri); } - /** Substitutes */ export function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) { - let accumulatedWS = 0; - result += before; - for (let i = start + before.length; i < end; i++) { - let ch = oldContent[i]; - if (ch === '\n' || ch === '\r') { - // only write new lines, skip the whitespace - accumulatedWS = 0; - result += ch; - } else { - accumulatedWS++; - } - } - result = append(result, ' ', accumulatedWS - after.length); - result += after; - return result; + let accumulatedWS = 0; + result += before; + for (let i = start + before.length; i < end; i++) { + let ch = oldContent[i]; + if (ch === '\n' || ch === '\r') { + // only write new lines, skip the whitespace + accumulatedWS = 0; + result += ch; + } else { + accumulatedWS++; + } + } + result = append(result, ' ', accumulatedWS - after.length); + result += after; + return result; } function append(result: string, str: string, n: number): string { - while (n > 0) { - if (n & 1) { - result += str; - } - n >>= 1; - str += str; - } - return result; + while (n > 0) { + if (n & 1) { + result += str; + } + n >>= 1; + str += str; + } + return result; } diff --git a/tools/vscode/packages/server/src/utils.ts b/tools/vscode/packages/server/src/utils.ts index c764aae13..f9f1acf34 100644 --- a/tools/vscode/packages/server/src/utils.ts +++ b/tools/vscode/packages/server/src/utils.ts @@ -4,68 +4,61 @@ import { Node } from 'vscode-html-languageservice'; /** Normalizes a document URI */ export function normalizeUri(uri: string): string { - return URI.parse(uri).toString(); + return URI.parse(uri).toString(); } /** Turns a URL into a normalized FS Path */ export function urlToPath(stringUrl: string): string | null { - const url = URI.parse(stringUrl); - if (url.scheme !== 'file') { - return null; - } - return url.fsPath.replace(/\\/g, '/'); + const url = URI.parse(stringUrl); + if (url.scheme !== 'file') { + return null; + } + return url.fsPath.replace(/\\/g, '/'); } /** Converts a path to a URL */ export function pathToUrl(path: string) { - return URI.file(path).toString(); + return URI.file(path).toString(); } - /** -* -* The language service is case insensitive, and would provide -* hover info for Svelte components like `Option` which have -* the same name like a html tag. -*/ + * + * The language service is case insensitive, and would provide + * hover info for Svelte components like `Option` which have + * the same name like a html tag. + */ export function isPossibleComponent(node: Node): boolean { - return !!node.tag?.[0].match(/[A-Z]/); + return !!node.tag?.[0].match(/[A-Z]/); } /** -* -* The language service is case insensitive, and would provide -* hover info for Svelte components like `Option` which have -* the same name like a html tag. -*/ + * + * The language service is case insensitive, and would provide + * hover info for Svelte components like `Option` which have + * the same name like a html tag. + */ export function isPossibleClientComponent(node: Node): boolean { - return isPossibleComponent(node) && (node.tag?.indexOf(':') ?? -1) > -1; + return isPossibleComponent(node) && (node.tag?.indexOf(':') ?? -1) > -1; } /** Flattens an array */ export function flatten<T>(arr: T[][]): T[] { - return arr.reduce((all, item) => [...all, ...item], []); + return arr.reduce((all, item) => [...all, ...item], []); } /** Clamps a number between min and max */ export function clamp(num: number, min: number, max: number): number { - return Math.max(min, Math.min(max, num)); + return Math.max(min, Math.min(max, num)); } /** Checks if a position is inside range */ export function isInRange(positionToTest: Position, range: Range): boolean { - return ( - isBeforeOrEqualToPosition(range.end, positionToTest) && - isBeforeOrEqualToPosition(positionToTest, range.start) - ); + return isBeforeOrEqualToPosition(range.end, positionToTest) && isBeforeOrEqualToPosition(positionToTest, range.start); } /** */ export function isBeforeOrEqualToPosition(position: Position, positionToTest: Position): boolean { - return ( - positionToTest.line < position.line || - (positionToTest.line === position.line && positionToTest.character <= position.character) - ); + return positionToTest.line < position.line || (positionToTest.line === position.line && positionToTest.character <= position.character); } /** @@ -76,23 +69,19 @@ export function isBeforeOrEqualToPosition(position: Position, positionToTest: Po * @param determineIfSame The function which determines if the previous invocation should be canceld or not * @param miliseconds Number of miliseconds to debounce */ -export function debounceSameArg<T>( - fn: (arg: T) => void, - shouldCancelPrevious: (newArg: T, prevArg?: T) => boolean, - miliseconds: number -): (arg: T) => void { - let timeout: any; - let prevArg: T | undefined; +export function debounceSameArg<T>(fn: (arg: T) => void, shouldCancelPrevious: (newArg: T, prevArg?: T) => boolean, miliseconds: number): (arg: T) => void { + let timeout: any; + let prevArg: T | undefined; - return (arg: T) => { - if (shouldCancelPrevious(arg, prevArg)) { - clearTimeout(timeout); - } + return (arg: T) => { + if (shouldCancelPrevious(arg, prevArg)) { + clearTimeout(timeout); + } - prevArg = arg; - timeout = setTimeout(() => { - fn(arg); - prevArg = undefined; - }, miliseconds); - }; + prevArg = arg; + timeout = setTimeout(() => { + fn(arg); + prevArg = undefined; + }, miliseconds); + }; } |