summaryrefslogtreecommitdiff
path: root/tools/language-server/src
diff options
context:
space:
mode:
Diffstat (limited to 'tools/language-server/src')
-rw-r--r--tools/language-server/src/core/config/ConfigManager.ts13
-rw-r--r--tools/language-server/src/core/config/index.ts1
-rw-r--r--tools/language-server/src/core/documents/Document.ts160
-rw-r--r--tools/language-server/src/core/documents/DocumentBase.ts141
-rw-r--r--tools/language-server/src/core/documents/DocumentManager.ts94
-rw-r--r--tools/language-server/src/core/documents/DocumentMapper.ts317
-rw-r--r--tools/language-server/src/core/documents/index.ts5
-rw-r--r--tools/language-server/src/core/documents/parseAstro.ts77
-rw-r--r--tools/language-server/src/core/documents/parseHtml.ts141
-rw-r--r--tools/language-server/src/core/documents/utils.ts250
-rw-r--r--tools/language-server/src/index.ts122
-rw-r--r--tools/language-server/src/plugins/PluginHost.ts178
-rw-r--r--tools/language-server/src/plugins/astro/AstroPlugin.ts339
-rw-r--r--tools/language-server/src/plugins/css/CSSDocument.ts95
-rw-r--r--tools/language-server/src/plugins/css/CSSPlugin.ts119
-rw-r--r--tools/language-server/src/plugins/css/StyleAttributeDocument.ts72
-rw-r--r--tools/language-server/src/plugins/css/features/getIdClassCompletion.ts67
-rw-r--r--tools/language-server/src/plugins/css/service.ts48
-rw-r--r--tools/language-server/src/plugins/html/HTMLPlugin.ts142
-rw-r--r--tools/language-server/src/plugins/index.ts6
-rw-r--r--tools/language-server/src/plugins/interfaces.ts171
-rw-r--r--tools/language-server/src/plugins/typescript/DocumentSnapshot.ts263
-rw-r--r--tools/language-server/src/plugins/typescript/LanguageServiceManager.ts85
-rw-r--r--tools/language-server/src/plugins/typescript/SnapshotManager.ts95
-rw-r--r--tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts189
-rw-r--r--tools/language-server/src/plugins/typescript/astro-sys.ts39
-rw-r--r--tools/language-server/src/plugins/typescript/features/CompletionsProvider.ts117
-rw-r--r--tools/language-server/src/plugins/typescript/features/HoverProvider.ts40
-rw-r--r--tools/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts129
-rw-r--r--tools/language-server/src/plugins/typescript/features/utils.ts54
-rw-r--r--tools/language-server/src/plugins/typescript/languageService.ts184
-rw-r--r--tools/language-server/src/plugins/typescript/module-loader.ts110
-rw-r--r--tools/language-server/src/plugins/typescript/previewer.ts125
-rw-r--r--tools/language-server/src/plugins/typescript/utils.ts239
-rw-r--r--tools/language-server/src/types/index.d.ts4
-rw-r--r--tools/language-server/src/utils.ts91
36 files changed, 0 insertions, 4322 deletions
diff --git a/tools/language-server/src/core/config/ConfigManager.ts b/tools/language-server/src/core/config/ConfigManager.ts
deleted file mode 100644
index 1e795ab96..000000000
--- a/tools/language-server/src/core/config/ConfigManager.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { VSCodeEmmetConfig } from 'vscode-emmet-helper';
-
-export class ConfigManager {
- private emmetConfig: VSCodeEmmetConfig = {};
-
- updateEmmetConfig(config: VSCodeEmmetConfig): void {
- this.emmetConfig = config || {};
- }
-
- getEmmetConfig(): VSCodeEmmetConfig {
- return this.emmetConfig;
- }
-}
diff --git a/tools/language-server/src/core/config/index.ts b/tools/language-server/src/core/config/index.ts
deleted file mode 100644
index cd869b795..000000000
--- a/tools/language-server/src/core/config/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './ConfigManager';
diff --git a/tools/language-server/src/core/documents/Document.ts b/tools/language-server/src/core/documents/Document.ts
deleted file mode 100644
index 04a460a08..000000000
--- a/tools/language-server/src/core/documents/Document.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-import type { TagInformation } from './utils';
-import { Position, Range } from 'vscode-languageserver';
-import { TextDocument } from 'vscode-languageserver-textdocument';
-import { HTMLDocument } from 'vscode-html-languageservice';
-
-import { clamp, urlToPath } from '../../utils';
-import { parseHtml } from './parseHtml';
-import { parseAstro, AstroDocument } from './parseAstro';
-import { extractStyleTag } from './utils';
-
-export class Document implements TextDocument {
- private content: string;
-
- languageId = 'astro';
- version = 0;
- html!: HTMLDocument;
- astro!: AstroDocument;
- styleInfo: TagInformation | null = null;
-
- constructor(public uri: string, text: string) {
- this.content = text;
- this.updateDocInfo();
- }
-
- private updateDocInfo() {
- this.html = parseHtml(this.content);
- this.astro = parseAstro(this.content);
- this.styleInfo = extractStyleTag(this.content, this.html);
- if (this.styleInfo) {
- this.styleInfo.attributes.lang = 'css';
- }
- }
-
- setText(text: string) {
- 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);
- }
-
- 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;
- }
-}
diff --git a/tools/language-server/src/core/documents/DocumentBase.ts b/tools/language-server/src/core/documents/DocumentBase.ts
deleted file mode 100644
index 299feeb62..000000000
--- a/tools/language-server/src/core/documents/DocumentBase.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import { clamp } from '../../utils';
-import { Position, TextDocument } from 'vscode-languageserver';
-
-/**
- * Represents a textual document.
- */
-export abstract class ReadableDocument implements TextDocument {
- /**
- * Get the text content of the document
- */
- abstract getText(): string;
-
- /**
- * Returns the url of the document
- */
- abstract getURL(): string;
-
- /**
- * Returns the file path if the url scheme is file
- */
- abstract getFilePath(): string | null;
-
- /**
- * Current version of the document.
- */
- public version = 0;
-
- /**
- * Get the length of the document's content
- */
- getTextLength(): number {
- return this.getText().length;
- }
-
- /**
- * Get the line and character based on the offset
- * @param offset The index of the position
- */
- positionAt(offset: number): Position {
- offset = clamp(offset, 0, this.getTextLength());
-
- const lineOffsets = this.getLineOffsets();
- let low = 0;
- let high = lineOffsets.length;
- if (high === 0) {
- return Position.create(0, offset);
- }
-
- while (low < high) {
- const mid = Math.floor((low + high) / 2);
- if (lineOffsets[mid] > offset) {
- high = mid;
- } else {
- low = mid + 1;
- }
- }
-
- // low is the least x for which the line offset is larger than the current offset
- // or array.length if no line offset is larger than the current offset
- const line = low - 1;
- return Position.create(line, offset - lineOffsets[line]);
- }
-
- /**
- * Get the index of the line and character position
- * @param position Line and character position
- */
- offsetAt(position: Position): number {
- const lineOffsets = this.getLineOffsets();
-
- if (position.line >= lineOffsets.length) {
- return this.getTextLength();
- } else if (position.line < 0) {
- return 0;
- }
-
- const lineOffset = lineOffsets[position.line];
- const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : this.getTextLength();
-
- return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
- }
-
- private getLineOffsets() {
- const lineOffsets = [];
- const text = this.getText();
- let isLineStart = true;
-
- for (let i = 0; i < text.length; i++) {
- if (isLineStart) {
- lineOffsets.push(i);
- isLineStart = false;
- }
- const ch = text.charAt(i);
- isLineStart = ch === '\r' || ch === '\n';
- if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
- i++;
- }
- }
-
- if (isLineStart && text.length > 0) {
- lineOffsets.push(text.length);
- }
-
- return lineOffsets;
- }
-
- /**
- * Implements TextDocument
- */
- get uri(): string {
- return this.getURL();
- }
-
- get lineCount(): number {
- return this.getText().split(/\r?\n/).length;
- }
-
- abstract languageId: string;
-}
-
-/**
- * Represents a textual document that can be manipulated.
- */
-export abstract class WritableDocument extends ReadableDocument {
- /**
- * Set the text content of the document
- * @param text The new text content
- */
- abstract setText(text: string): void;
-
- /**
- * Update the text between two positions.
- * @param text The new text slice
- * @param start Start offset of the new text
- * @param end End offset of the new text
- */
- update(text: string, start: number, end: number): void {
- const content = this.getText();
- this.setText(content.slice(0, start) + text + content.slice(end));
- }
-}
diff --git a/tools/language-server/src/core/documents/DocumentManager.ts b/tools/language-server/src/core/documents/DocumentManager.ts
deleted file mode 100644
index 7c9c168c1..000000000
--- a/tools/language-server/src/core/documents/DocumentManager.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { EventEmitter } from 'events';
-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));
- }
-
- 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);
-
- return document;
- }
-
- closeDocument(uri: string) {
- uri = normalizeUri(uri);
-
- const document = this.documents.get(uri);
- if (!document) {
- throw new Error('Cannot call methods on an unopened 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);
- }
-
- 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');
- }
-
- 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);
- }
-
- markAsOpenedInClient(uri: string) {
- this.openedInClient.add(normalizeUri(uri));
- }
-
- getAllOpenedByClient() {
- return Array.from(this.documents.entries()).filter((doc) => this.openedInClient.has(doc[0]));
- }
-
- 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/language-server/src/core/documents/DocumentMapper.ts b/tools/language-server/src/core/documents/DocumentMapper.ts
deleted file mode 100644
index 8a6a6ef29..000000000
--- a/tools/language-server/src/core/documents/DocumentMapper.ts
+++ /dev/null
@@ -1,317 +0,0 @@
-import {
- Position,
- Range,
- CompletionItem,
- Hover,
- Diagnostic,
- ColorPresentation,
- SymbolInformation,
- LocationLink,
- TextDocumentEdit,
- CodeAction,
- SelectionRange,
- TextEdit,
- InsertReplaceEdit,
-} from 'vscode-languageserver';
-import { TagInformation, offsetAt, positionAt } from './utils';
-import { SourceMapConsumer } from 'source-map';
-
-export interface DocumentMapper {
- /**
- * Map the generated position to the original position
- * @param generatedPosition Position in fragment
- */
- getOriginalPosition(generatedPosition: Position): Position;
-
- /**
- * Map the original position to the generated position
- * @param originalPosition Position in parent
- */
- getGeneratedPosition(originalPosition: Position): Position;
-
- /**
- * Returns true if the given original position is inside of the generated map
- * @param pos Position in original
- */
- isInGenerated(pos: Position): boolean;
-
- /**
- * Get document URL
- */
- getURL(): string;
-
- /**
- * Implement this if you need teardown logic before this mapper gets cleaned up.
- */
- destroy?(): void;
-}
-
-/**
- * Does not map, returns positions as is.
- */
-export class IdentityMapper implements DocumentMapper {
- constructor(private url: string, private parent?: DocumentMapper) {}
-
- getOriginalPosition(generatedPosition: Position): Position {
- if (this.parent) {
- generatedPosition = this.getOriginalPosition(generatedPosition);
- }
-
- return generatedPosition;
- }
-
- getGeneratedPosition(originalPosition: Position): Position {
- if (this.parent) {
- originalPosition = this.getGeneratedPosition(originalPosition);
- }
-
- return originalPosition;
- }
-
- isInGenerated(position: Position): boolean {
- if (this.parent && !this.parent.isInGenerated(position)) {
- return false;
- }
-
- return true;
- }
-
- getURL(): string {
- return this.url;
- }
-
- destroy() {
- this.parent?.destroy?.();
- }
-}
-
-/**
- * Maps positions in a fragment relative to a parent.
- */
-export class FragmentMapper implements DocumentMapper {
- constructor(private originalText: string, private tagInfo: TagInformation, private url: string) {}
-
- getOriginalPosition(generatedPosition: Position): Position {
- const parentOffset = this.offsetInParent(offsetAt(generatedPosition, this.tagInfo.content));
- return positionAt(parentOffset, this.originalText);
- }
-
- private offsetInParent(offset: number): number {
- return this.tagInfo.start + offset;
- }
-
- getGeneratedPosition(originalPosition: Position): Position {
- const fragmentOffset = offsetAt(originalPosition, this.originalText) - this.tagInfo.start;
- return positionAt(fragmentOffset, this.tagInfo.content);
- }
-
- isInGenerated(pos: Position): boolean {
- const offset = offsetAt(pos, this.originalText);
- return offset >= this.tagInfo.start && offset <= this.tagInfo.end;
- }
-
- getURL(): string {
- return this.url;
- }
-}
-
-export class SourceMapDocumentMapper implements DocumentMapper {
- constructor(protected consumer: SourceMapConsumer, protected sourceUri: string, private parent?: DocumentMapper) {}
-
- getOriginalPosition(generatedPosition: Position): Position {
- if (this.parent) {
- generatedPosition = this.parent.getOriginalPosition(generatedPosition);
- }
-
- if (generatedPosition.line < 0) {
- return { line: -1, character: -1 };
- }
-
- const mapped = this.consumer.originalPositionFor({
- line: generatedPosition.line + 1,
- column: generatedPosition.character,
- });
-
- if (!mapped) {
- return { line: -1, character: -1 };
- }
-
- if (mapped.line === 0) {
- console.log('Got 0 mapped line from', generatedPosition, 'col was', mapped.column);
- }
-
- return {
- line: (mapped.line || 0) - 1,
- character: mapped.column || 0,
- };
- }
-
- getGeneratedPosition(originalPosition: Position): Position {
- if (this.parent) {
- originalPosition = this.parent.getGeneratedPosition(originalPosition);
- }
-
- const mapped = this.consumer.generatedPositionFor({
- line: originalPosition.line + 1,
- column: originalPosition.character,
- source: this.sourceUri,
- });
-
- if (!mapped) {
- return { line: -1, character: -1 };
- }
-
- const result = {
- line: (mapped.line || 0) - 1,
- character: mapped.column || 0,
- };
-
- if (result.line < 0) {
- return result;
- }
-
- return result;
- }
-
- isInGenerated(position: Position): boolean {
- if (this.parent && !this.isInGenerated(position)) {
- return false;
- }
-
- const generated = this.getGeneratedPosition(position);
- return generated.line >= 0;
- }
-
- getURL(): string {
- return this.sourceUri;
- }
-
- /**
- * Needs to be called when source mapper is no longer needed in order to prevent memory leaks.
- */
- destroy() {
- this.parent?.destroy?.();
- this.consumer.destroy();
- }
-}
-
-export function mapRangeToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, range: Range): Range {
- // DON'T use Range.create here! Positions might not be mapped
- // and therefore return negative numbers, which makes Range.create throw.
- // These invalid position need to be handled
- // on a case-by-case basis in the calling functions.
- const originalRange = {
- start: fragment.getOriginalPosition(range.start),
- end: fragment.getOriginalPosition(range.end),
- };
-
- // Range may be mapped one character short - reverse that for "in the same line" cases
- if (
- originalRange.start.line === originalRange.end.line &&
- range.start.line === range.end.line &&
- originalRange.end.character - originalRange.start.character === range.end.character - range.start.character - 1
- ) {
- originalRange.end.character += 1;
- }
-
- return originalRange;
-}
-
-export function mapRangeToGenerated(fragment: DocumentMapper, range: Range): Range {
- return Range.create(fragment.getGeneratedPosition(range.start), fragment.getGeneratedPosition(range.end));
-}
-
-export function mapCompletionItemToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, item: CompletionItem): CompletionItem {
- if (!item.textEdit) {
- return item;
- }
-
- return {
- ...item,
- textEdit: mapEditToOriginal(fragment, item.textEdit),
- };
-}
-
-export function mapHoverToParent(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, hover: Hover): Hover {
- if (!hover.range) {
- return hover;
- }
-
- return { ...hover, range: mapRangeToOriginal(fragment, hover.range) };
-}
-
-export function mapObjWithRangeToOriginal<T extends { range: Range }>(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, objWithRange: T): T {
- return { ...objWithRange, range: mapRangeToOriginal(fragment, objWithRange.range) };
-}
-
-export function mapInsertReplaceEditToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, edit: InsertReplaceEdit): InsertReplaceEdit {
- return {
- ...edit,
- insert: mapRangeToOriginal(fragment, edit.insert),
- replace: mapRangeToOriginal(fragment, edit.replace),
- };
-}
-
-export function mapEditToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, edit: TextEdit | InsertReplaceEdit): TextEdit | InsertReplaceEdit {
- return TextEdit.is(edit) ? mapObjWithRangeToOriginal(fragment, edit) : mapInsertReplaceEditToOriginal(fragment, edit);
-}
-
-export function mapDiagnosticToGenerated(fragment: DocumentMapper, diagnostic: Diagnostic): Diagnostic {
- return { ...diagnostic, range: mapRangeToGenerated(fragment, diagnostic.range) };
-}
-
-export function mapColorPresentationToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, presentation: ColorPresentation): ColorPresentation {
- const item = {
- ...presentation,
- };
-
- if (item.textEdit) {
- item.textEdit = mapObjWithRangeToOriginal(fragment, item.textEdit);
- }
-
- if (item.additionalTextEdits) {
- item.additionalTextEdits = item.additionalTextEdits.map((edit) => mapObjWithRangeToOriginal(fragment, edit));
- }
-
- return item;
-}
-
-export function mapSymbolInformationToOriginal(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, info: SymbolInformation): SymbolInformation {
- return { ...info, location: mapObjWithRangeToOriginal(fragment, info.location) };
-}
-
-export function mapLocationLinkToOriginal(fragment: DocumentMapper, def: LocationLink): LocationLink {
- return LocationLink.create(
- def.targetUri,
- fragment.getURL() === def.targetUri ? mapRangeToOriginal(fragment, def.targetRange) : def.targetRange,
- fragment.getURL() === def.targetUri ? mapRangeToOriginal(fragment, def.targetSelectionRange) : def.targetSelectionRange,
- def.originSelectionRange ? mapRangeToOriginal(fragment, def.originSelectionRange) : undefined
- );
-}
-
-export function mapTextDocumentEditToOriginal(fragment: DocumentMapper, edit: TextDocumentEdit) {
- if (edit.textDocument.uri !== fragment.getURL()) {
- return edit;
- }
-
- return TextDocumentEdit.create(
- edit.textDocument,
- edit.edits.map((textEdit) => mapObjWithRangeToOriginal(fragment, textEdit))
- );
-}
-
-export function mapCodeActionToOriginal(fragment: DocumentMapper, codeAction: CodeAction) {
- return CodeAction.create(
- codeAction.title,
- {
- documentChanges: codeAction.edit!.documentChanges!.map((edit) => mapTextDocumentEditToOriginal(fragment, edit as TextDocumentEdit)),
- },
- codeAction.kind
- );
-}
-
-export function mapSelectionRangeToParent(fragment: Pick<DocumentMapper, 'getOriginalPosition'>, selectionRange: SelectionRange): SelectionRange {
- const { range, parent } = selectionRange;
-
- return SelectionRange.create(mapRangeToOriginal(fragment, range), parent && mapSelectionRangeToParent(fragment, parent));
-}
diff --git a/tools/language-server/src/core/documents/index.ts b/tools/language-server/src/core/documents/index.ts
deleted file mode 100644
index 5dc0eb61f..000000000
--- a/tools/language-server/src/core/documents/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export * from './Document';
-export * from './DocumentBase';
-export * from './DocumentManager';
-export * from './DocumentMapper';
-export * from './utils';
diff --git a/tools/language-server/src/core/documents/parseAstro.ts b/tools/language-server/src/core/documents/parseAstro.ts
deleted file mode 100644
index 71c7764d8..000000000
--- a/tools/language-server/src/core/documents/parseAstro.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { getFirstNonWhitespaceIndex } from './utils';
-
-interface Frontmatter {
- state: null | 'open' | 'closed';
- startOffset: null | number;
- endOffset: null | number;
-}
-
-interface Content {
- firstNonWhitespaceOffset: null | number;
-}
-
-export interface AstroDocument {
- 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),
- };
-}
-
-/** 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';
- }
- }
- 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();
-
- 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 };
- }
- }
-}
diff --git a/tools/language-server/src/core/documents/parseHtml.ts b/tools/language-server/src/core/documents/parseHtml.ts
deleted file mode 100644
index f5de5f292..000000000
--- a/tools/language-server/src/core/documents/parseHtml.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import { getLanguageService, HTMLDocument, TokenType, ScannerState, Scanner, Node, Position } from 'vscode-html-languageservice';
-import { Document } from './Document';
-import { isInsideExpression } from './utils';
-
-const parser = getLanguageService();
-
-/**
- * Parses text as HTML
- */
-export function parseHtml(text: string): HTMLDocument {
- const preprocessed = preprocess(text);
-
- // We can safely only set getText because only this is used for parsing
- const parsedDoc = parser.parseHTMLDocument(<any>{ getText: () => preprocessed });
-
- return parsedDoc;
-}
-
-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;
-
- 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.StartTagSelfClose) {
- 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);
- }
-
- token = scanner.scan();
- }
-
- return text;
-
- 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];
-}
-
-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;
- }
-
- 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;
- }
- token = scanner.scan();
- }
-
- return null;
-}
-
-function inStartTag(offset: number, node: Node) {
- return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd;
-}
diff --git a/tools/language-server/src/core/documents/utils.ts b/tools/language-server/src/core/documents/utils.ts
deleted file mode 100644
index eb9d2060d..000000000
--- a/tools/language-server/src/core/documents/utils.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-import { HTMLDocument, Node, Position } from 'vscode-html-languageservice';
-import { Range } from 'vscode-languageserver';
-import { clamp, isInRange } from '../../utils';
-import { parseHtml } from './parseHtml';
-
-export interface TagInformation {
- content: string;
- attributes: Record<string, string>;
- start: number;
- end: number;
- startPos: Position;
- endPos: Position;
- container: { start: number; end: number };
-}
-
-function parseAttributes(rawAttrs: Record<string, string | null> | undefined): Record<string, string> {
- const attrs: Record<string, string> = {};
- if (!rawAttrs) {
- return attrs;
- }
-
- Object.keys(rawAttrs).forEach((attrName) => {
- const attrValue = rawAttrs[attrName];
- attrs[attrName] = attrValue === null ? attrName : removeOuterQuotes(attrValue);
- });
- return attrs;
-
- function removeOuterQuotes(attrValue: string) {
- if ((attrValue.startsWith('"') && attrValue.endsWith('"')) || (attrValue.startsWith("'") && attrValue.endsWith("'"))) {
- return attrValue.slice(1, attrValue.length - 1);
- }
- return attrValue;
- }
-}
-
-/**
- * Gets word range at position.
- * 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 };
-}
-
-/**
- * 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);
-}
-
-/**
- * Gets index of first-non-whitespace character.
- */
-export function getFirstNonWhitespaceIndex(str: string): number {
- 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('}');
-}
-
-/**
- * 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 isInTag(position: Position, tagInfo: TagInformation | null): tagInfo is TagInformation {
- return !!tagInfo && isInRange(position, Range.create(tagInfo.startPos, tagInfo.endPos));
-}
-
-/**
- * Get the line and character based on the offset
- * @param offset The index of the position
- * @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;
- }
- }
-
- // 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 offset of the line and character position
- * @param position Line and character position
- * @param text The text for which the offset should be retrived
- */
-export function offsetAt(position: Position, text: string): number {
- const lineOffsets = getLineOffsets(text);
-
- 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;
-
- 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++;
- }
- }
-
- if (isLineStart && text.length > 0) {
- lineOffsets.push(text.length);
- }
-
- return lineOffsets;
-}
-
-export function* walk(node: Node): Generator<Node, void, unknown> {
- for (let child of node.children) {
- yield* walk(child);
- }
- yield node;
-}
-
-/*
-export function* walk(node: Node, startIndex = 0) {
- let skip, tmp;
- let depth = 0;
- let index = startIndex;
-
- // Always start with the initial element.
- do {
- if ( !skip && (tmp = node.firstChild) ) {
- depth++;
- callback('child', node, tmp, index);
- index++;
- } else if ( tmp = node.nextSibling ) {
- skip = false;
- callback('sibling', node, tmp, index);
- index++;
- } else {
- tmp = node.parentNode;
- depth--;
- skip = true;
- }
- node = tmp;
- } while ( depth > 0 );
-};
-*/
-
-/**
- * Extracts a tag (style or script) from the given text
- * and returns its start, end and the attributes on that tag.
- *
- * @param source text content to extract tag from
- * @param tag the tag to extract
- */
-function extractTags(text: string, tag: 'script' | 'style' | 'template', html?: HTMLDocument): TagInformation[] {
- const rootNodes = html?.roots || parseHtml(text).roots;
- const matchedNodes = rootNodes.filter((node) => node.tag === tag);
-
- if (tag === 'style' && !matchedNodes.length && rootNodes.length && rootNodes[0].tag === 'html') {
- for (let child of walk(rootNodes[0])) {
- if (child.tag === 'style') {
- matchedNodes.push(child);
- }
- }
- }
-
- return matchedNodes.map(transformToTagInfo);
-
- function transformToTagInfo(matchedNode: Node) {
- const start = matchedNode.startTagEnd ?? matchedNode.start;
- const end = matchedNode.endTagStart ?? matchedNode.end;
- const startPos = positionAt(start, text);
- const endPos = positionAt(end, text);
- const container = {
- start: matchedNode.start,
- end: matchedNode.end,
- };
- const content = text.substring(start, end);
-
- return {
- content,
- attributes: parseAttributes(matchedNode.attributes),
- start,
- end,
- startPos,
- endPos,
- container,
- };
- }
-}
-
-export function extractStyleTag(source: string, html?: HTMLDocument): TagInformation | null {
- const styles = extractTags(source, 'style', html);
- if (!styles.length) {
- return null;
- }
-
- // There can only be one style tag
- return styles[0];
-}
diff --git a/tools/language-server/src/index.ts b/tools/language-server/src/index.ts
deleted file mode 100644
index e029684cb..000000000
--- a/tools/language-server/src/index.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { RequestType, TextDocumentPositionParams, createConnection, ProposedFeatures, TextDocumentSyncKind, TextDocumentIdentifier } from 'vscode-languageserver';
-import { Document, DocumentManager } from './core/documents';
-import { ConfigManager } from './core/config';
-import { PluginHost, CSSPlugin, HTMLPlugin, TypeScriptPlugin, AppCompletionItem, AstroPlugin } from './plugins';
-import { urlToPath } from './utils';
-
-const TagCloseRequest: RequestType<TextDocumentPositionParams, string | null, any> = new RequestType('html/tag');
-
-/**
- * Starts `astro-languageservice`
- */
-export function startServer() {
- let connection = createConnection(ProposedFeatures.all);
-
- const docManager = new DocumentManager(({ uri, text }: { uri: string; text: string }) => new Document(uri, text));
- const configManager = new ConfigManager();
- const pluginHost = new PluginHost(docManager);
-
- connection.onInitialize((evt) => {
- const workspaceUris = evt.workspaceFolders?.map((folder) => folder.uri.toString()) ?? [evt.rootUri ?? ''];
-
- pluginHost.initialize({
- filterIncompleteCompletions: !evt.initializationOptions?.dontFilterIncompleteCompletions,
- definitionLinkSupport: !!evt.capabilities.textDocument?.definition?.linkSupport,
- });
- pluginHost.register(new HTMLPlugin(docManager, configManager));
- pluginHost.register(new CSSPlugin(docManager, configManager));
- pluginHost.register(new TypeScriptPlugin(docManager, configManager, workspaceUris));
- pluginHost.register(new AstroPlugin(docManager, configManager, workspaceUris));
- configManager.updateEmmetConfig(evt.initializationOptions?.configuration?.emmet || evt.initializationOptions?.emmetConfig || {});
-
- return {
- capabilities: {
- textDocumentSync: TextDocumentSyncKind.Incremental,
- foldingRangeProvider: true,
- definitionProvider: true,
- completionProvider: {
- resolveProvider: true,
- triggerCharacters: [
- '.',
- '"',
- "'",
- '`',
- '/',
- '@',
- '<',
-
- // Emmet
- '>',
- '*',
- '#',
- '$',
- '+',
- '^',
- '(',
- '[',
- '@',
- '-',
- // No whitespace because
- // it makes for weird/too many completions
- // of other completion providers
-
- // Astro
- ':',
- ],
- },
- hoverProvider: true,
- signatureHelpProvider: {
- triggerCharacters: ['(', ',', '<'],
- retriggerCharacters: [')'],
- },
- },
- };
- });
-
- // Documents
- connection.onDidOpenTextDocument((evt) => {
- docManager.openDocument(evt.textDocument);
- docManager.markAsOpenedInClient(evt.textDocument.uri);
- });
-
- connection.onDidCloseTextDocument((evt) => docManager.closeDocument(evt.textDocument.uri));
-
- 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);
-
- pluginHost.onWatchFileChanges(params);
- });
-
- // Config
- connection.onDidChangeConfiguration(({ settings }) => {
- configManager.updateEmmetConfig(settings.emmet);
- });
-
- // Features
- connection.onCompletion((evt) => pluginHost.getCompletions(evt.textDocument, evt.position, evt.context));
- connection.onCompletionResolve((completionItem) => {
- const data = (completionItem as AppCompletionItem).data as TextDocumentIdentifier;
-
- if (!data) {
- return completionItem;
- }
-
- return pluginHost.resolveCompletion(data, completionItem);
- });
- connection.onHover((evt) => pluginHost.doHover(evt.textDocument, evt.position));
- connection.onDefinition((evt) => pluginHost.getDefinitions(evt.textDocument, evt.position));
- connection.onFoldingRanges((evt) => pluginHost.getFoldingRanges(evt.textDocument));
- connection.onRequest(TagCloseRequest, (evt: any) => pluginHost.doTagComplete(evt.textDocument, evt.position));
- connection.onSignatureHelp((evt, cancellationToken) => pluginHost.getSignatureHelp(evt.textDocument, evt.position, evt.context, cancellationToken));
-
- connection.listen();
-}
diff --git a/tools/language-server/src/plugins/PluginHost.ts b/tools/language-server/src/plugins/PluginHost.ts
deleted file mode 100644
index 3ad21a21c..000000000
--- a/tools/language-server/src/plugins/PluginHost.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-import type {
- CancellationToken,
- CompletionContext,
- CompletionItem,
- DefinitionLink,
- Location,
- Position,
- SignatureHelp,
- SignatureHelpContext,
- TextDocumentIdentifier,
-} from 'vscode-languageserver';
-import type { DocumentManager } from '../core/documents';
-import type * as d from './interfaces';
-import { flatten } from '../utils';
-import { CompletionList } from 'vscode-languageserver';
-import { Hover, FoldingRange } from 'vscode-languageserver-types';
-
-enum ExecuteMode {
- None,
- FirstNonNull,
- Collect,
-}
-
-interface PluginHostConfig {
- filterIncompleteCompletions: boolean;
- definitionLinkSupport: boolean;
-}
-
-export class PluginHost {
- private plugins: d.Plugin[] = [];
- private pluginHostConfig: PluginHostConfig = {
- filterIncompleteCompletions: true,
- definitionLinkSupport: false,
- };
-
- constructor(private documentsManager: DocumentManager) {}
-
- initialize(pluginHostConfig: PluginHostConfig) {
- this.pluginHostConfig = pluginHostConfig;
- }
-
- register(plugin: d.Plugin) {
- this.plugins.push(plugin);
- }
-
- 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');
- }
-
- 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 resolveCompletion(textDocument: TextDocumentIdentifier, completionItem: d.AppCompletionItem): Promise<CompletionItem> {
- 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);
-
- return result ?? completionItem;
- }
-
- async doHover(textDocument: TextDocumentIdentifier, position: Position): Promise<Hover | null> {
- const document = this.getDocument(textDocument.uri);
- if (!document) {
- throw new Error('Cannot call methods on an unopened document');
- }
-
- return this.execute<Hover>('doHover', [document, position], ExecuteMode.FirstNonNull);
- }
-
- 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');
- }
-
- return this.execute<string | null>('doTagComplete', [document, position], ExecuteMode.FirstNonNull);
- }
-
- 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 foldingRanges = flatten(await this.execute<FoldingRange[]>('getFoldingRanges', [document], ExecuteMode.Collect)).filter((completion) => completion != null);
-
- return foldingRanges;
- }
-
- async getDefinitions(textDocument: TextDocumentIdentifier, position: Position): Promise<DefinitionLink[] | Location[]> {
- const document = this.getDocument(textDocument.uri);
- if (!document) {
- throw new Error('Cannot call methods on an unopened document');
- }
-
- const definitions = flatten(await this.execute<DefinitionLink[]>('getDefinitions', [document, position], ExecuteMode.Collect));
-
- if (this.pluginHostConfig.definitionLinkSupport) {
- return definitions;
- } else {
- return definitions.map((def) => <Location>{ range: def.targetSelectionRange, uri: def.targetUri });
- }
- }
-
- async getSignatureHelp(
- textDocument: TextDocumentIdentifier,
- position: Position,
- context: SignatureHelpContext | undefined,
- cancellationToken: CancellationToken
- ): Promise<SignatureHelp | null> {
- const document = this.getDocument(textDocument.uri);
- if (!document) {
- throw new Error('Cannot call methods on an unopened document');
- }
-
- return await this.execute<any>('getSignatureHelp', [document, position, context, cancellationToken], ExecuteMode.FirstNonNull);
- }
-
- 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) => {
- let ret = this.tryExecutePlugin(plugin, name, args, []);
- return ret;
- })
- );
- 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/language-server/src/plugins/astro/AstroPlugin.ts b/tools/language-server/src/plugins/astro/AstroPlugin.ts
deleted file mode 100644
index 495f1859d..000000000
--- a/tools/language-server/src/plugins/astro/AstroPlugin.ts
+++ /dev/null
@@ -1,339 +0,0 @@
-import { DefinitionLink } from 'vscode-languageserver';
-import type { Document, DocumentManager } from '../../core/documents';
-import type { ConfigManager } from '../../core/config';
-import type { CompletionsProvider, AppCompletionList, FoldingRangeProvider } from '../interfaces';
-import {
- CompletionContext,
- Position,
- CompletionList,
- CompletionItem,
- CompletionItemKind,
- CompletionTriggerKind,
- InsertTextFormat,
- LocationLink,
- FoldingRange,
- MarkupContent,
- MarkupKind,
- Range,
- TextEdit,
-} from 'vscode-languageserver';
-import { Node } from 'vscode-html-languageservice';
-import { isPossibleClientComponent, pathToUrl, urlToPath } from '../../utils';
-import { toVirtualAstroFilePath } from '../typescript/utils';
-import { isInsideFrontmatter } from '../../core/documents/utils';
-import * as ts from 'typescript';
-import { LanguageServiceManager as TypeScriptLanguageServiceManager } from '../typescript/LanguageServiceManager';
-import { ensureRealFilePath } from '../typescript/utils';
-import { FoldingRangeKind } from 'vscode-languageserver-types';
-
-export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
- private readonly docManager: DocumentManager;
- private readonly configManager: ConfigManager;
- private readonly tsLanguageServiceManager: TypeScriptLanguageServiceManager;
- public pluginName = 'Astro';
-
- constructor(docManager: DocumentManager, configManager: ConfigManager, workspaceUris: string[]) {
- this.docManager = docManager;
- this.configManager = configManager;
- this.tsLanguageServiceManager = new TypeScriptLanguageServiceManager(docManager, configManager, workspaceUris);
- }
-
- async getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList | null> {
- const doc = this.docManager.get(document.uri);
- if (!doc) return null;
-
- let items: CompletionItem[] = [];
-
- if (completionContext?.triggerCharacter === '-') {
- const frontmatter = this.getComponentScriptCompletion(doc, position, completionContext);
- if (frontmatter) items.push(frontmatter);
- }
-
- if (completionContext?.triggerCharacter === ':') {
- const clientHint = this.getClientHintCompletion(doc, position, completionContext);
- if (clientHint) items.push(...clientHint);
- }
-
- if (!this.isInsideFrontmatter(document, position)) {
- const props = await this.getPropCompletions(document, position, completionContext);
- if (props.length) {
- items.push(...props);
- }
- }
-
- return CompletionList.create(items, true);
- }
-
- async getFoldingRanges(document: Document): Promise<FoldingRange[]> {
- const foldingRanges: FoldingRange[] = [];
- const { frontmatter } = document.astro;
-
- // Currently editing frontmatter, don't fold
- if (frontmatter.state !== 'closed') return foldingRanges;
-
- const start = document.positionAt(frontmatter.startOffset as number);
- const end = document.positionAt((frontmatter.endOffset as number) - 3);
- return [
- {
- startLine: start.line,
- startCharacter: start.character,
- endLine: end.line,
- endCharacter: end.character,
- kind: FoldingRangeKind.Imports,
- },
- ];
- }
-
- async getDefinitions(document: Document, position: Position): Promise<DefinitionLink[]> {
- if (this.isInsideFrontmatter(document, position)) {
- return [];
- }
-
- const offset = document.offsetAt(position);
- const html = document.html;
-
- const node = html.findNodeAt(offset);
- if (!this.isComponentTag(node)) {
- return [];
- }
-
- const [componentName] = node.tag!.split(':');
-
- const { lang } = await this.tsLanguageServiceManager.getTypeScriptDoc(document);
- const defs = this.getDefinitionsForComponentName(document, lang, componentName);
-
- if (!defs) {
- return [];
- }
-
- const startRange: Range = Range.create(Position.create(0, 0), Position.create(0, 0));
- const links = defs.map((def) => {
- const defFilePath = ensureRealFilePath(def.fileName);
- return LocationLink.create(pathToUrl(defFilePath), startRange, startRange);
- });
-
- return links;
- }
-
- private getClientHintCompletion(document: Document, position: Position, completionContext?: CompletionContext): CompletionItem[] | null {
- const node = document.html.findNodeAt(document.offsetAt(position));
- if (!isPossibleClientComponent(node)) return null;
-
- return [
- {
- label: ':load',
- insertText: 'load',
- commitCharacters: ['l'],
- },
- {
- label: ':idle',
- insertText: 'idle',
- commitCharacters: ['i'],
- },
- {
- label: ':visible',
- insertText: 'visible',
- commitCharacters: ['v'],
- },
- ];
- }
-
- private getComponentScriptCompletion(document: Document, position: Position, completionContext?: CompletionContext): CompletionItem | null {
- const base = {
- kind: CompletionItemKind.Snippet,
- label: '---',
- sortText: '\0',
- preselect: true,
- detail: 'Component script',
- insertTextFormat: InsertTextFormat.Snippet,
- commitCharacters: ['-'],
- };
- const prefix = document.getLineUntilOffset(document.offsetAt(position));
-
- if (document.astro.frontmatter.state === null) {
- return {
- ...base,
- insertText: '---\n$0\n---',
- textEdit: prefix.match(/^\s*\-+/) ? TextEdit.replace({ start: { ...position, character: 0 }, end: position }, '---\n$0\n---') : undefined,
- };
- }
- if (document.astro.frontmatter.state === 'open') {
- return {
- ...base,
- insertText: '---',
- textEdit: prefix.match(/^\s*\-+/) ? TextEdit.replace({ start: { ...position, character: 0 }, end: position }, '---') : undefined,
- };
- }
- return null;
- }
-
- private async getPropCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise<CompletionItem[]> {
- const offset = document.offsetAt(position);
- const html = document.html;
-
- const node = html.findNodeAt(offset);
- if (!this.isComponentTag(node)) {
- return [];
- }
- const inAttribute = node.start + node.tag!.length < offset;
- if (!inAttribute) {
- return [];
- }
-
- // If inside of attributes, skip.
- if (completionContext && completionContext.triggerKind === CompletionTriggerKind.TriggerCharacter && completionContext.triggerCharacter === '"') {
- return [];
- }
-
- const componentName = node.tag!;
- const { lang: thisLang } = await this.tsLanguageServiceManager.getTypeScriptDoc(document);
-
- const defs = this.getDefinitionsForComponentName(document, thisLang, componentName);
-
- if (!defs) {
- return [];
- }
-
- const defFilePath = ensureRealFilePath(defs[0].fileName);
-
- const lang = await this.tsLanguageServiceManager.getTypeScriptLangForPath(defFilePath);
- const program = lang.getProgram();
- const sourceFile = program?.getSourceFile(toVirtualAstroFilePath(defFilePath));
- const typeChecker = program?.getTypeChecker();
-
- if (!sourceFile || !typeChecker) {
- return [];
- }
-
- let propsNode = this.getPropsNode(sourceFile);
- if (!propsNode) {
- return [];
- }
-
- const completionItems: CompletionItem[] = [];
-
- for (let type of typeChecker.getBaseTypes(propsNode as unknown as ts.InterfaceType)) {
- type.symbol.members!.forEach((mem) => {
- let item: CompletionItem = {
- label: mem.name,
- insertText: mem.name,
- commitCharacters: [],
- };
-
- mem.getDocumentationComment(typeChecker);
- let description = mem
- .getDocumentationComment(typeChecker)
- .map((val) => val.text)
- .join('\n');
-
- if (description) {
- let docs: MarkupContent = {
- kind: MarkupKind.Markdown,
- value: description,
- };
- item.documentation = docs;
- }
- completionItems.push(item);
- });
- }
-
- for (let member of propsNode.members) {
- if (!member.name) continue;
-
- let name = member.name.getText();
- let symbol = typeChecker.getSymbolAtLocation(member.name);
- if (!symbol) continue;
- let description = symbol
- .getDocumentationComment(typeChecker)
- .map((val) => val.text)
- .join('\n');
-
- let item: CompletionItem = {
- label: name,
- insertText: name,
- commitCharacters: [],
- };
-
- if (description) {
- let docs: MarkupContent = {
- kind: MarkupKind.Markdown,
- value: description,
- };
- item.documentation = docs;
- }
-
- completionItems.push(item);
- }
-
- return completionItems;
- }
-
- private isInsideFrontmatter(document: Document, position: Position) {
- return isInsideFrontmatter(document.getText(), document.offsetAt(position));
- }
-
- private isComponentTag(node: Node): boolean {
- if (!node.tag) {
- return false;
- }
- const firstChar = node.tag[0];
- return /[A-Z]/.test(firstChar);
- }
-
- private getDefinitionsForComponentName(document: Document, lang: ts.LanguageService, componentName: string): readonly ts.DefinitionInfo[] | undefined {
- const filePath = urlToPath(document.uri);
- const tsFilePath = toVirtualAstroFilePath(filePath!);
-
- const sourceFile = lang.getProgram()?.getSourceFile(tsFilePath);
- if (!sourceFile) {
- return undefined;
- }
-
- const specifier = this.getImportSpecifierForIdentifier(sourceFile, componentName);
- if (!specifier) {
- return [];
- }
-
- const defs = lang.getDefinitionAtPosition(tsFilePath, specifier.getStart());
- if (!defs) {
- return undefined;
- }
-
- return defs;
- }
-
- private getImportSpecifierForIdentifier(sourceFile: ts.SourceFile, identifier: string): ts.Expression | undefined {
- let importSpecifier: ts.Expression | undefined = undefined;
- ts.forEachChild(sourceFile, (tsNode) => {
- if (ts.isImportDeclaration(tsNode)) {
- if (tsNode.importClause) {
- const { name } = tsNode.importClause;
- if (name && name.getText() === identifier) {
- importSpecifier = tsNode.moduleSpecifier;
- return true;
- }
- }
- }
- });
- return importSpecifier;
- }
-
- private getPropsNode(sourceFile: ts.SourceFile): ts.InterfaceDeclaration | null {
- let found: ts.InterfaceDeclaration | null = null;
- ts.forEachChild(sourceFile, (node) => {
- if (isNodeExported(node)) {
- if (ts.isInterfaceDeclaration(node)) {
- if (ts.getNameOfDeclaration(node)?.getText() === 'Props') {
- found = node;
- }
- }
- }
- });
-
- return found;
- }
-}
-
-function isNodeExported(node: ts.Node): boolean {
- return (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 || (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile);
-}
diff --git a/tools/language-server/src/plugins/css/CSSDocument.ts b/tools/language-server/src/plugins/css/CSSDocument.ts
deleted file mode 100644
index 9f1839678..000000000
--- a/tools/language-server/src/plugins/css/CSSDocument.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { Stylesheet, TextDocument } from 'vscode-css-languageservice';
-import { Position } from 'vscode-languageserver';
-import { getLanguageService } from './service';
-import { Document, DocumentMapper, ReadableDocument, TagInformation } from '../../core/documents/index';
-
-export interface CSSDocumentBase extends DocumentMapper, TextDocument {
- languageId: string;
- stylesheet: Stylesheet;
-}
-
-export class CSSDocument extends ReadableDocument implements DocumentMapper {
- private styleInfo: Pick<TagInformation, 'attributes' | 'start' | 'end'>;
- readonly version = this.parent.version;
-
- public stylesheet: Stylesheet;
- public languageId: string;
-
- constructor(private parent: Document) {
- super();
-
- if (this.parent.styleInfo) {
- this.styleInfo = this.parent.styleInfo;
- } else {
- this.styleInfo = {
- attributes: {},
- start: -1,
- end: -1,
- };
- }
-
- this.languageId = this.language;
- this.stylesheet = getLanguageService(this.language).parseStylesheet(this);
- }
-
- /**
- * Get the fragment position relative to the parent
- * @param pos Position in fragment
- */
- getOriginalPosition(pos: Position): Position {
- const parentOffset = this.styleInfo.start + this.offsetAt(pos);
- return this.parent.positionAt(parentOffset);
- }
-
- /**
- * Get the position relative to the start of the fragment
- * @param pos Position in parent
- */
- getGeneratedPosition(pos: Position): Position {
- const fragmentOffset = this.parent.offsetAt(pos) - this.styleInfo.start;
- return this.positionAt(fragmentOffset);
- }
-
- /**
- * Returns true if the given parent position is inside of this fragment
- * @param pos Position in parent
- */
- isInGenerated(pos: Position): boolean {
- const offset = this.parent.offsetAt(pos);
- return offset >= this.styleInfo.start && offset <= this.styleInfo.end;
- }
-
- /**
- * Get the fragment text from the parent
- */
- getText(): string {
- return this.parent.getText().slice(this.styleInfo.start, this.styleInfo.end);
- }
-
- /**
- * Returns the length of the fragment as calculated from the start and end positon
- */
- getTextLength(): number {
- return this.styleInfo.end - this.styleInfo.start;
- }
-
- /**
- * Return the parent file path
- */
- getFilePath(): string | null {
- return this.parent.getFilePath();
- }
-
- getURL() {
- return this.parent.getURL();
- }
-
- getAttributes() {
- return this.styleInfo.attributes;
- }
-
- private get language() {
- const attrs = this.getAttributes();
- return attrs.lang || attrs.type || 'css';
- }
-}
diff --git a/tools/language-server/src/plugins/css/CSSPlugin.ts b/tools/language-server/src/plugins/css/CSSPlugin.ts
deleted file mode 100644
index 3083edc56..000000000
--- a/tools/language-server/src/plugins/css/CSSPlugin.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import type { CompletionsProvider } from '../interfaces';
-import type { Document, DocumentManager } from '../../core/documents';
-import type { ConfigManager } from '../../core/config';
-import { getEmmetCompletionParticipants, doComplete as doEmmetComplete } from 'vscode-emmet-helper';
-import { CompletionContext, CompletionList, CompletionTriggerKind, Position } from 'vscode-languageserver';
-import { isInsideFrontmatter } from '../../core/documents/utils';
-import { CSSDocument, CSSDocumentBase } from './CSSDocument';
-import { getLanguage, getLanguageService } from './service';
-import { StyleAttributeDocument } from './StyleAttributeDocument';
-import { mapCompletionItemToOriginal } from '../../core/documents';
-import { AttributeContext, getAttributeContextAtPosition } from '../../core/documents/parseHtml';
-import { getIdClassCompletion } from './features/getIdClassCompletion';
-
-export class CSSPlugin implements CompletionsProvider {
- private docManager: DocumentManager;
- private configManager: ConfigManager;
- private documents = new WeakMap<Document, CSSDocument>();
- private triggerCharacters = new Set(['.', ':', '-', '/']);
- public pluginName = 'CSS';
-
- constructor(docManager: DocumentManager, configManager: ConfigManager) {
- this.docManager = docManager;
- this.configManager = configManager;
-
- this.docManager.on('documentChange', (document) => {
- this.documents.set(document, new CSSDocument(document));
- });
- }
-
- getCompletions(document: Document, position: Position, completionContext?: CompletionContext): CompletionList | null {
- const triggerCharacter = completionContext?.triggerCharacter;
- const triggerKind = completionContext?.triggerKind;
- const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter;
-
- if (isCustomTriggerCharacter && triggerCharacter && !this.triggerCharacters.has(triggerCharacter)) {
- return null;
- }
-
- if (this.isInsideFrontmatter(document, position)) {
- return null;
- }
-
- const cssDocument = this.getCSSDoc(document);
-
- if (cssDocument.isInGenerated(position)) {
- return this.getCompletionsInternal(document, position, cssDocument);
- }
-
- const attributeContext = getAttributeContextAtPosition(document, position);
- if (!attributeContext) {
- return null;
- }
-
- if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
- const [start, end] = attributeContext.valueRange;
- return this.getCompletionsInternal(document, position, new StyleAttributeDocument(document, start, end));
- } else {
- return getIdClassCompletion(cssDocument, attributeContext);
- }
- }
-
- private getCompletionsInternal(document: Document, position: Position, cssDocument: CSSDocumentBase) {
- if (isSASS(cssDocument)) {
- // the css language service does not support sass, still we can use
- // the emmet helper directly to at least get emmet completions
- return doEmmetComplete(document, position, 'sass', this.configManager.getEmmetConfig());
- }
-
- const type = extractLanguage(cssDocument);
-
- const lang = getLanguageService(type);
- const emmetResults: CompletionList = {
- isIncomplete: true,
- items: [],
- };
- if (false /* this.configManager.getConfig().css.completions.emmet */) {
- lang.setCompletionParticipants([
- getEmmetCompletionParticipants(cssDocument, cssDocument.getGeneratedPosition(position), getLanguage(type), this.configManager.getEmmetConfig(), emmetResults),
- ]);
- }
- const results = lang.doComplete(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
- return CompletionList.create(
- [...(results ? results.items : []), ...emmetResults.items].map((completionItem) => mapCompletionItemToOriginal(cssDocument, completionItem)),
- // Emmet completions change on every keystroke, so they are never complete
- emmetResults.items.length > 0
- );
- }
-
- private inStyleAttributeWithoutInterpolation(attrContext: AttributeContext, text: string): attrContext is Required<AttributeContext> {
- return attrContext.name === 'style' && !!attrContext.valueRange && !text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{');
- }
-
- private getCSSDoc(document: Document) {
- let cssDoc = this.documents.get(document);
- if (!cssDoc || cssDoc.version < document.version) {
- cssDoc = new CSSDocument(document);
- this.documents.set(document, cssDoc);
- }
- return cssDoc;
- }
-
- private isInsideFrontmatter(document: Document, position: Position) {
- return isInsideFrontmatter(document.getText(), document.offsetAt(position));
- }
-}
-
-function isSASS(document: CSSDocumentBase) {
- switch (extractLanguage(document)) {
- case 'sass':
- return true;
- default:
- return false;
- }
-}
-
-function extractLanguage(document: CSSDocumentBase): string {
- const lang = document.languageId;
- return lang.replace(/^text\//, '');
-}
diff --git a/tools/language-server/src/plugins/css/StyleAttributeDocument.ts b/tools/language-server/src/plugins/css/StyleAttributeDocument.ts
deleted file mode 100644
index e00398037..000000000
--- a/tools/language-server/src/plugins/css/StyleAttributeDocument.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { Stylesheet } from 'vscode-css-languageservice';
-import { Position } from 'vscode-languageserver';
-import { getLanguageService } from './service';
-import { Document, DocumentMapper, ReadableDocument } from '../../core/documents';
-
-const PREFIX = '__ {';
-const SUFFIX = '}';
-
-export class StyleAttributeDocument extends ReadableDocument implements DocumentMapper {
- readonly version = this.parent.version;
-
- public stylesheet: Stylesheet;
- public languageId = 'css';
-
- constructor(private readonly parent: Document, private readonly attrStart: number, private readonly attrEnd: number) {
- super();
-
- this.stylesheet = getLanguageService(this.languageId).parseStylesheet(this);
- }
-
- /**
- * Get the fragment position relative to the parent
- * @param pos Position in fragment
- */
- getOriginalPosition(pos: Position): Position {
- const parentOffset = this.attrStart + this.offsetAt(pos) - PREFIX.length;
- return this.parent.positionAt(parentOffset);
- }
-
- /**
- * Get the position relative to the start of the fragment
- * @param pos Position in parent
- */
- getGeneratedPosition(pos: Position): Position {
- const fragmentOffset = this.parent.offsetAt(pos) - this.attrStart + PREFIX.length;
- return this.positionAt(fragmentOffset);
- }
-
- /**
- * Returns true if the given parent position is inside of this fragment
- * @param pos Position in parent
- */
- isInGenerated(pos: Position): boolean {
- const offset = this.parent.offsetAt(pos);
- return offset >= this.attrStart && offset <= this.attrEnd;
- }
-
- /**
- * Get the fragment text from the parent
- */
- getText(): string {
- return PREFIX + this.parent.getText().slice(this.attrStart, this.attrEnd) + SUFFIX;
- }
-
- /**
- * Returns the length of the fragment as calculated from the start and end position
- */
- getTextLength(): number {
- return PREFIX.length + this.attrEnd - this.attrStart + SUFFIX.length;
- }
-
- /**
- * Return the parent file path
- */
- getFilePath(): string | null {
- return this.parent.getFilePath();
- }
-
- getURL() {
- return this.parent.getURL();
- }
-}
diff --git a/tools/language-server/src/plugins/css/features/getIdClassCompletion.ts b/tools/language-server/src/plugins/css/features/getIdClassCompletion.ts
deleted file mode 100644
index 45acb5ad6..000000000
--- a/tools/language-server/src/plugins/css/features/getIdClassCompletion.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { CompletionItem, CompletionItemKind, CompletionList } from 'vscode-languageserver';
-import { AttributeContext } from '../../../core/documents/parseHtml';
-import { CSSDocument } from '../CSSDocument';
-
-export function getIdClassCompletion(cssDoc: CSSDocument, attributeContext: AttributeContext): CompletionList | null {
- const collectingType = getCollectingType(attributeContext);
-
- if (!collectingType) {
- return null;
- }
- const items = collectSelectors(cssDoc.stylesheet as CSSNode, collectingType);
-
- console.log('getIdClassCompletion items', items.length);
- return CompletionList.create(items);
-}
-
-function getCollectingType(attributeContext: AttributeContext): number | undefined {
- if (attributeContext.inValue) {
- if (attributeContext.name === 'class') {
- return NodeType.ClassSelector;
- }
- if (attributeContext.name === 'id') {
- return NodeType.IdentifierSelector;
- }
- } else if (attributeContext.name.startsWith('class:')) {
- return NodeType.ClassSelector;
- }
-}
-
-/**
- * incomplete see
- * https://github.com/microsoft/vscode-css-languageservice/blob/master/src/parser/cssNodes.ts#L14
- * The enum is not exported. we have to update this whenever it changes
- */
-export enum NodeType {
- ClassSelector = 14,
- IdentifierSelector = 15,
-}
-
-export type CSSNode = {
- type: number;
- children: CSSNode[] | undefined;
- getText(): string;
-};
-
-export function collectSelectors(stylesheet: CSSNode, type: number) {
- const result: CSSNode[] = [];
- walk(stylesheet, (node) => {
- if (node.type === type) {
- result.push(node);
- }
- });
-
- return result.map(
- (node): CompletionItem => ({
- label: node.getText().substring(1),
- kind: CompletionItemKind.Keyword,
- })
- );
-}
-
-function walk(node: CSSNode, callback: (node: CSSNode) => void) {
- callback(node);
- if (node.children) {
- node.children.forEach((node) => walk(node, callback));
- }
-}
diff --git a/tools/language-server/src/plugins/css/service.ts b/tools/language-server/src/plugins/css/service.ts
deleted file mode 100644
index 78b11296e..000000000
--- a/tools/language-server/src/plugins/css/service.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageService, ICSSDataProvider } from 'vscode-css-languageservice';
-
-const customDataProvider: ICSSDataProvider = {
- providePseudoClasses() {
- return [];
- },
- provideProperties() {
- return [];
- },
- provideAtDirectives() {
- return [];
- },
- providePseudoElements() {
- return [];
- },
-};
-
-const [css, scss, less] = [getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService].map((getService) =>
- getService({
- customDataProviders: [customDataProvider],
- })
-);
-
-const langs = {
- css,
- scss,
- less,
-};
-
-export function getLanguage(kind?: string) {
- switch (kind) {
- case 'scss':
- case 'text/scss':
- return 'scss' as const;
- case 'less':
- case 'text/less':
- return 'less' as const;
- case 'css':
- case 'text/css':
- default:
- return 'css' as const;
- }
-}
-
-export function getLanguageService(kind?: string): LanguageService {
- const lang = getLanguage(kind);
- return langs[lang];
-}
diff --git a/tools/language-server/src/plugins/html/HTMLPlugin.ts b/tools/language-server/src/plugins/html/HTMLPlugin.ts
deleted file mode 100644
index 49ca46b86..000000000
--- a/tools/language-server/src/plugins/html/HTMLPlugin.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-import { CompletionsProvider, FoldingRangeProvider } from '../interfaces';
-import { getEmmetCompletionParticipants, VSCodeEmmetConfig } from 'vscode-emmet-helper';
-import { getLanguageService, HTMLDocument, CompletionItem as HtmlCompletionItem, Node, FoldingRange } from 'vscode-html-languageservice';
-import { CompletionList, Position, CompletionItem, CompletionItemKind, TextEdit } from 'vscode-languageserver';
-import type { Document, DocumentManager } from '../../core/documents';
-import { isInsideExpression, isInsideFrontmatter } from '../../core/documents/utils';
-import type { ConfigManager } from '../../core/config';
-
-export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider {
- private lang = getLanguageService();
- private documents = new WeakMap<Document, HTMLDocument>();
- private styleScriptTemplate = new Set(['template', 'style', 'script']);
- private configManager: ConfigManager;
- public pluginName = 'HTML';
-
- constructor(docManager: DocumentManager, configManager: ConfigManager) {
- docManager.on('documentChange', (document) => {
- this.documents.set(document, document.html);
- });
- this.configManager = configManager;
- }
-
- getCompletions(document: Document, position: Position): CompletionList | null {
- const html = this.documents.get(document);
-
- if (!html) {
- return null;
- }
-
- if (this.isInsideFrontmatter(document, position) || this.isInsideExpression(html, document, position)) {
- return null;
- }
-
- const offset = document.offsetAt(position);
- const node = html.findNodeAt(offset);
-
- if (this.isComponentTag(node)) {
- return null;
- }
-
- const emmetResults: CompletionList = {
- isIncomplete: true,
- items: [],
- };
- 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);
-
- return CompletionList.create(
- [...this.toCompletionItems(items), ...this.getLangCompletions(items), ...emmetResults.items],
- // Emmet completions change on every keystroke, so they are never complete
- emmetResults.items.length > 0
- );
- }
-
- 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 {
- const html = this.documents.get(document);
- if (!html) {
- return null;
- }
-
- if (this.isInsideFrontmatter(document, position) || this.isInsideExpression(html, document, position)) {
- return null;
- }
-
- return this.lang.doTagComplete(document, position, html);
- }
-
- /**
- * The HTML language service uses newer types which clash
- * without the stable ones. Transform to the stable types.
- */
- private toCompletionItems(items: HtmlCompletionItem[]): CompletionItem[] {
- return items.map((item) => {
- if (!item.textEdit || TextEdit.is(item.textEdit)) {
- return item as CompletionItem;
- }
- return {
- ...item,
- textEdit: TextEdit.replace(item.textEdit.replace, item.textEdit.newText),
- };
- });
- }
-
- private getLangCompletions(completions: CompletionItem[]): CompletionItem[] {
- const styleScriptTemplateCompletions = completions.filter((completion) => completion.kind === CompletionItemKind.Property && this.styleScriptTemplate.has(completion.label));
- const langCompletions: CompletionItem[] = [];
- addLangCompletion('style', ['scss', 'sass']);
- return langCompletions;
-
- /** Add language completions */
- function addLangCompletion(tag: string, languages: string[]) {
- const existingCompletion = styleScriptTemplateCompletions.find((completion) => completion.label === tag);
- if (!existingCompletion) {
- return;
- }
-
- languages.forEach((lang) =>
- langCompletions.push({
- ...existingCompletion,
- label: `${tag} (lang="${lang}")`,
- insertText: existingCompletion.insertText && `${existingCompletion.insertText} lang="${lang}"`,
- textEdit:
- existingCompletion.textEdit && TextEdit.is(existingCompletion.textEdit)
- ? {
- range: existingCompletion.textEdit.range,
- newText: `${existingCompletion.textEdit.newText} lang="${lang}"`,
- }
- : undefined,
- })
- );
- }
- }
-
- private isInsideExpression(html: HTMLDocument, document: Document, position: Position) {
- const offset = document.offsetAt(position);
- const node = html.findNodeAt(offset);
- return isInsideExpression(document.getText(), node.start, offset);
- }
-
- private isInsideFrontmatter(document: Document, position: Position) {
- return isInsideFrontmatter(document.getText(), document.offsetAt(position));
- }
-
- private isComponentTag(node: Node): boolean {
- if (!node.tag) {
- return false;
- }
- const firstChar = node.tag[0];
- return /[A-Z]/.test(firstChar);
- }
-}
diff --git a/tools/language-server/src/plugins/index.ts b/tools/language-server/src/plugins/index.ts
deleted file mode 100644
index bb73cbe5e..000000000
--- a/tools/language-server/src/plugins/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export * from './PluginHost';
-export * from './astro/AstroPlugin';
-export * from './html/HTMLPlugin';
-export * from './typescript/TypeScriptPlugin';
-export * from './interfaces';
-export * from './css/CSSPlugin';
diff --git a/tools/language-server/src/plugins/interfaces.ts b/tools/language-server/src/plugins/interfaces.ts
deleted file mode 100644
index 84e4cbda3..000000000
--- a/tools/language-server/src/plugins/interfaces.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-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,
-} 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;
-}
-
-export interface AppCompletionList<T extends TextDocumentIdentifier = any> extends CompletionList {
- items: Array<AppCompletionItem<T>>;
-}
-
-export interface DiagnosticsProvider {
- getDiagnostics(document: Document): Resolvable<Diagnostic[]>;
-}
-
-export interface HoverProvider {
- doHover(document: Document, position: Position): Resolvable<Hover | null>;
-}
-
-export interface FoldingRangeProvider {
- getFoldingRanges(document: Document): Resolvable<FoldingRange[] | null>;
-}
-
-export interface CompletionsProvider<T extends TextDocumentIdentifier = any> {
- getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Resolvable<AppCompletionList<T> | null>;
-
- resolveCompletion?(document: Document, completionItem: AppCompletionItem<T>): Resolvable<AppCompletionItem<T>>;
-}
-
-export interface FormattingProvider {
- formatDocument(document: Document, options: FormattingOptions): Resolvable<TextEdit[]>;
-}
-
-export interface TagCompleteProvider {
- doTagComplete(document: Document, position: Position): Resolvable<string | null>;
-}
-
-export interface DocumentColorsProvider {
- getDocumentColors(document: Document): Resolvable<ColorInformation[]>;
-}
-
-export interface ColorPresentationsProvider {
- getColorPresentations(document: Document, range: Range, color: Color): Resolvable<ColorPresentation[]>;
-}
-
-export interface DocumentSymbolsProvider {
- getDocumentSymbols(document: Document): Resolvable<SymbolInformation[]>;
-}
-
-export interface DefinitionsProvider {
- getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[]>;
-}
-
-export interface BackwardsCompatibleDefinitionsProvider {
- 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>;
-}
-
-export interface FileRename {
- oldUri: string;
- newUri: string;
-}
-
-export interface UpdateImportsProvider {
- 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>;
-}
-
-export interface FindReferencesProvider {
- findReferences(document: Document, position: Position, context: ReferenceContext): Promise<Location[] | null>;
-}
-
-export interface SignatureHelpProvider {
- getSignatureHelp(document: Document, position: Position, context: SignatureHelpContext | undefined): Resolvable<SignatureHelp | null>;
-}
-
-export interface SelectionRangeProvider {
- getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>;
-}
-
-export interface SemanticTokensProvider {
- getSemanticTokens(textDocument: Document, range?: Range): Resolvable<SemanticTokens | null>;
-}
-
-export interface LinkedEditingRangesProvider {
- getLinkedEditingRanges(document: Document, position: Position): Resolvable<LinkedEditingRanges | null>;
-}
-
-export interface OnWatchFileChangesPara {
- fileName: string;
- changeType: FileChangeType;
-}
-
-export interface OnWatchFileChanges {
- onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void;
-}
-
-export interface UpdateTsOrJsFile {
- updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void;
-}
-
-type ProviderBase = DiagnosticsProvider &
- 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;
-}
-
-interface NamedPlugin {
- pluginName: string;
-}
-
-export type Plugin = Partial<NamedPlugin & ProviderBase & DefinitionsProvider & OnWatchFileChanges & SelectionRangeProvider & UpdateTsOrJsFile>;
diff --git a/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts b/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts
deleted file mode 100644
index 89f8c400e..000000000
--- a/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-import * as ts from 'typescript';
-import { readFileSync } from 'fs';
-import { TextDocumentContentChangeEvent, Position } from 'vscode-languageserver';
-import { Document, DocumentMapper, IdentityMapper } from '../../core/documents';
-import { isInTag, positionAt, offsetAt } from '../../core/documents/utils';
-import { pathToUrl } from '../../utils';
-import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } from './utils';
-
-const ASTRO_DEFINITION = readFileSync(require.resolve('../../../astro.d.ts'));
-
-/**
- * The mapper to get from original snapshot positions to generated and vice versa.
- */
-export interface SnapshotFragment extends DocumentMapper {
- positionAt(offset: number): Position;
- offsetAt(position: Position): number;
-}
-
-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;
-}
-
-export const createDocumentSnapshot = (filePath: string, currentText: string | null, createDocument?: (_filePath: string, text: string) => Document): DocumentSnapshot => {
- const text = currentText || (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);
-};
-
-class AstroDocumentSnapshot implements DocumentSnapshot {
- version = this.doc.version;
- scriptKind = ts.ScriptKind.Unknown;
-
- constructor(private doc: Document) {}
-
- async getFragment(): Promise<DocumentFragmentSnapshot> {
- const uri = pathToUrl(this.filePath);
- const mapper = await this.getMapper(uri);
- return new DocumentFragmentSnapshot(mapper, this.doc);
- }
-
- async destroyFragment() {
- return;
- }
-
- get text() {
- let raw = this.doc.getText();
- return this.transformContent(raw);
- }
-
- /** @internal */
- private transformContent(content: string) {
- return (
- content.replace(/---/g, '///') +
- // Add TypeScript definitions
- ASTRO_DEFINITION
- );
- }
-
- get filePath() {
- return this.doc.getFilePath() || '';
- }
-
- 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);
- }
-
- private getMapper(uri: string) {
- return new IdentityMapper(uri);
- }
-}
-
-export class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFragment' | 'destroyFragment'>, SnapshotFragment {
- version: number;
- filePath: string;
- url: string;
- text: string;
-
- scriptKind = ts.ScriptKind.TSX;
- scriptInfo = null;
-
- constructor(private mapper: any, private parent: Document) {
- const filePath = parent.getFilePath();
- if (!filePath) throw new Error('Cannot create a document fragment from a non-local document');
- const text = parent.getText();
- this.version = parent.version;
- this.filePath = toVirtualAstroFilePath(filePath);
- this.url = toVirtualAstroFilePath(filePath);
- this.text = this.transformContent(text);
- }
-
- /** @internal */
- private transformContent(content: string) {
- return (
- content.replace(/---/g, '///') +
- // Add TypeScript definitions
- ASTRO_DEFINITION
- );
- }
-
- 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);
- }
-
- getOriginalPosition(pos: Position): Position {
- return this.mapper.getOriginalPosition(pos);
- }
-
- getGeneratedPosition(pos: Position): Position {
- return this.mapper.getGeneratedPosition(pos);
- }
-
- isInGenerated(pos: Position): boolean {
- return !isInTag(pos, this.parent.styleInfo);
- }
-
- getURL(): string {
- return this.url;
- }
-}
-
-export 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;
- }
-
- getOriginalPosition(pos: Position): Position {
- return pos;
- }
-
- 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/language-server/src/plugins/typescript/LanguageServiceManager.ts b/tools/language-server/src/plugins/typescript/LanguageServiceManager.ts
deleted file mode 100644
index 9ff71abf7..000000000
--- a/tools/language-server/src/plugins/typescript/LanguageServiceManager.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import * as ts from 'typescript';
-import type { Document, DocumentManager } from '../../core/documents';
-import type { ConfigManager } from '../../core/config';
-import { urlToPath, pathToUrl, debounceSameArg } from '../../utils';
-import { getLanguageService, getLanguageServiceForPath, getLanguageServiceForDocument, LanguageServiceContainer, LanguageServiceDocumentContext } from './languageService';
-import { SnapshotManager } from './SnapshotManager';
-import { DocumentSnapshot } from './DocumentSnapshot';
-
-export class LanguageServiceManager {
- private readonly docManager: DocumentManager;
- private readonly configManager: ConfigManager;
- private readonly workspaceUris: string[];
- private docContext: LanguageServiceDocumentContext;
-
- constructor(docManager: DocumentManager, configManager: ConfigManager, workspaceUris: string[]) {
- this.docManager = docManager;
- this.configManager = configManager;
- this.workspaceUris = workspaceUris;
- this.docContext = {
- getWorkspaceRoot: (fileName: string) => this.getWorkspaceRoot(fileName),
- createDocument: this.createDocument,
- };
-
- const handleDocumentChange = (document: Document) => {
- // This refreshes the document in the ts language service
- this.getTypeScriptDoc(document);
- };
-
- docManager.on(
- 'documentChange',
- debounceSameArg(handleDocumentChange, (newDoc, prevDoc) => newDoc.uri === prevDoc?.uri, 1000)
- );
- docManager.on('documentOpen', handleDocumentChange);
- }
-
- private getWorkspaceRoot(fileName: string) {
- if (this.workspaceUris.length === 1) return urlToPath(this.workspaceUris[0]) as string;
- return this.workspaceUris.reduce((found, curr) => {
- const url = urlToPath(curr) as string;
- if (fileName.startsWith(url) && curr.length < url.length) return url;
- return found;
- }, '');
- }
-
- private createDocument = (fileName: string, content: string) => {
- const uri = pathToUrl(fileName);
- const document = this.docManager.openDocument({
- languageId: 'astro',
- version: 0,
- text: content,
- uri,
- });
- return document;
- };
-
- async getSnapshot(document: Document): Promise<DocumentSnapshot>;
- async getSnapshot(pathOrDoc: string | Document): Promise<DocumentSnapshot>;
- async getSnapshot(pathOrDoc: string | Document) {
- const filePath = typeof pathOrDoc === 'string' ? pathOrDoc : pathOrDoc.getFilePath() || '';
- const tsService = await this.getTypeScriptLanguageService(filePath);
- return tsService.updateDocument(pathOrDoc);
- }
-
- async getTypeScriptDoc(document: Document): Promise<{
- tsDoc: DocumentSnapshot;
- lang: ts.LanguageService;
- }> {
- const lang = await getLanguageServiceForDocument(document, this.workspaceUris, this.docContext);
- const tsDoc = await this.getSnapshot(document);
-
- return { tsDoc, lang };
- }
-
- async getTypeScriptLangForPath(filePath: string): Promise<ts.LanguageService> {
- return getLanguageServiceForPath(filePath, this.workspaceUris, this.docContext);
- }
-
- async getSnapshotManager(filePath: string): Promise<SnapshotManager> {
- return (await this.getTypeScriptLanguageService(filePath)).snapshotManager;
- }
-
- private getTypeScriptLanguageService(filePath: string): Promise<LanguageServiceContainer> {
- return getLanguageService(filePath, this.workspaceUris, this.docContext);
- }
-}
diff --git a/tools/language-server/src/plugins/typescript/SnapshotManager.ts b/tools/language-server/src/plugins/typescript/SnapshotManager.ts
deleted file mode 100644
index 5a406b945..000000000
--- a/tools/language-server/src/plugins/typescript/SnapshotManager.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import * as ts from 'typescript';
-import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
-import { toVirtualAstroFilePath } from './utils';
-import { DocumentSnapshot, TypeScriptDocumentSnapshot, createDocumentSnapshot } from './DocumentSnapshot';
-
-export interface TsFilesSpec {
- 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) {}
-
- updateProjectFiles() {
- const { include, exclude } = this.fileSpec;
-
- if (include?.length === 0) return;
-
- const projectFiles = ts.sys.readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include);
-
- this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles]));
- }
-
- 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, null);
-
- 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}`
- );
- }
- }
-}
diff --git a/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts
deleted file mode 100644
index db5e701b4..000000000
--- a/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import type { ConfigManager } from '../../core/config';
-import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces';
-import type { CancellationToken, Hover, SignatureHelp, SignatureHelpContext } from 'vscode-languageserver';
-import { join as pathJoin, dirname as pathDirname } from 'path';
-import { Document, DocumentManager, isInsideFrontmatter } from '../../core/documents';
-import { SourceFile, ImportDeclaration, Node, SyntaxKind } from 'typescript';
-import { CompletionContext, DefinitionLink, FileChangeType, Position, LocationLink } from 'vscode-languageserver';
-import * as ts from 'typescript';
-import { LanguageServiceManager } from './LanguageServiceManager';
-import { SnapshotManager } from './SnapshotManager';
-import { convertToLocationRange, isVirtualAstroFilePath, isVirtualFilePath, getScriptKindFromFileName } from './utils';
-import { isNotNullOrUndefined, pathToUrl } from '../../utils';
-import { CompletionsProviderImpl, CompletionEntryWithIdentifer } from './features/CompletionsProvider';
-import { HoverProviderImpl } from './features/HoverProvider';
-import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
-import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider';
-
-type BetterTS = typeof ts & {
- getTouchingPropertyName(sourceFile: SourceFile, pos: number): Node;
-};
-
-export class TypeScriptPlugin implements CompletionsProvider {
- private readonly docManager: DocumentManager;
- private readonly configManager: ConfigManager;
- private readonly languageServiceManager: LanguageServiceManager;
- public pluginName = 'TypeScript';
-
- private readonly completionProvider: CompletionsProviderImpl;
- private readonly hoverProvider: HoverProviderImpl;
- private readonly signatureHelpProvider: SignatureHelpProviderImpl;
-
- 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);
- this.hoverProvider = new HoverProviderImpl(this.languageServiceManager);
- this.signatureHelpProvider = new SignatureHelpProviderImpl(this.languageServiceManager);
- }
-
- async doHover(document: Document, position: Position): Promise<Hover | null> {
- return this.hoverProvider.doHover(document, position);
- }
-
- async getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> {
- const completions = await this.completionProvider.getCompletions(document, position, completionContext);
-
- return completions;
- }
-
- async resolveCompletion(document: Document, completionItem: AppCompletionItem<CompletionEntryWithIdentifer>): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> {
- return this.completionProvider.resolveCompletion(document, completionItem);
- }
-
- async getDefinitions(document: Document, position: Position): Promise<DefinitionLink[]> {
- if (!this.isInsideFrontmatter(document, position)) {
- return [];
- }
-
- const { lang, tsDoc } = await this.languageServiceManager.getTypeScriptDoc(document);
- const mainFragment = await tsDoc.getFragment();
-
- const filePath = tsDoc.filePath;
- const tsFilePath = filePath.endsWith('.ts') ? filePath : filePath + '.ts';
-
- const fragmentPosition = mainFragment.getGeneratedPosition(position);
- const fragmentOffset = mainFragment.offsetAt(fragmentPosition);
-
- let defs = lang.getDefinitionAndBoundSpan(tsFilePath, fragmentOffset);
-
- if (!defs || !defs.definitions) {
- return [];
- }
-
- // Resolve all imports if we can
- if (this.goToDefinitionFoundOnlyAlias(tsFilePath, defs.definitions!)) {
- let importDef = this.getGoToDefinitionRefsForImportSpecifier(tsFilePath, fragmentOffset, lang);
- if (importDef) {
- defs = importDef;
- }
- }
-
- const docs = new SnapshotFragmentMap(this.languageServiceManager);
- docs.set(tsDoc.filePath, { fragment: mainFragment, snapshot: tsDoc });
-
- const result = await Promise.all(
- defs.definitions!.map(async (def) => {
- const { fragment, snapshot } = await docs.retrieve(def.fileName);
-
- if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) {
- const fileName = isVirtualFilePath(def.fileName) ? def.fileName.substr(0, def.fileName.length - 3) : def.fileName;
- const textSpan = isVirtualAstroFilePath(tsFilePath) ? { start: 0, length: 0 } : def.textSpan;
- return LocationLink.create(
- pathToUrl(fileName),
- convertToLocationRange(fragment, textSpan),
- convertToLocationRange(fragment, textSpan),
- convertToLocationRange(mainFragment, defs!.textSpan)
- );
- }
- })
- );
- return result.filter(isNotNullOrUndefined);
- }
-
- async onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void> {
- const doneUpdateProjectFiles = new Set<SnapshotManager>();
-
- for (const { fileName, changeType } of onWatchFileChangesParams) {
- const scriptKind = getScriptKindFromFileName(fileName);
-
- if (scriptKind === ts.ScriptKind.Unknown) {
- // We don't deal with svelte files here
- continue;
- }
-
- 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;
- }
-
- snapshotManager.updateProjectFile(fileName);
- }
- }
-
- async getSignatureHelp(document: Document, position: Position, context: SignatureHelpContext | undefined, cancellationToken?: CancellationToken): Promise<SignatureHelp | null> {
- return this.signatureHelpProvider.getSignatureHelp(document, position, context, cancellationToken);
- }
-
- /**
- *
- * @internal
- */
- public async getSnapshotManager(fileName: string) {
- return this.languageServiceManager.getSnapshotManager(fileName);
- }
-
- private isInsideFrontmatter(document: Document, position: Position) {
- return isInsideFrontmatter(document.getText(), document.offsetAt(position));
- }
-
- private goToDefinitionFoundOnlyAlias(tsFileName: string, defs: readonly ts.DefinitionInfo[]) {
- return !!(defs.length === 1 && defs[0].kind === 'alias' && defs[0].fileName === tsFileName);
- }
-
- private getGoToDefinitionRefsForImportSpecifier(tsFilePath: string, offset: number, lang: ts.LanguageService): ts.DefinitionInfoAndBoundSpan | undefined {
- const program = lang.getProgram();
- const sourceFile = program?.getSourceFile(tsFilePath);
- if (sourceFile) {
- let node = (ts as BetterTS).getTouchingPropertyName(sourceFile, offset);
- if (node && node.kind === SyntaxKind.Identifier) {
- if (node.parent.kind === SyntaxKind.ImportClause) {
- let decl = node.parent.parent as ImportDeclaration;
- let spec = ts.isStringLiteral(decl.moduleSpecifier) && decl.moduleSpecifier.text;
- if (spec) {
- let fileName = pathJoin(pathDirname(tsFilePath), spec);
- let start = node.pos + 1;
- let def: ts.DefinitionInfoAndBoundSpan = {
- definitions: [
- {
- kind: 'alias',
- fileName,
- name: '',
- containerKind: '',
- containerName: '',
- textSpan: {
- start: 0,
- length: 0,
- },
- } as ts.DefinitionInfo,
- ],
- textSpan: {
- start,
- length: node.end - start,
- },
- };
- return def;
- }
- }
- }
- }
- }
-}
diff --git a/tools/language-server/src/plugins/typescript/astro-sys.ts b/tools/language-server/src/plugins/typescript/astro-sys.ts
deleted file mode 100644
index c8d23254d..000000000
--- a/tools/language-server/src/plugins/typescript/astro-sys.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import * as ts from 'typescript';
-import { DocumentSnapshot } from './SnapshotManager';
-import { ensureRealAstroFilePath, isAstroFilePath, isVirtualAstroFilePath, toRealAstroFilePath } from './utils';
-
-/**
- * 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) {
- 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', '.svelte', '.vue']);
- 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);
- };
- }
-
- return AstroSys;
-}
diff --git a/tools/language-server/src/plugins/typescript/features/CompletionsProvider.ts b/tools/language-server/src/plugins/typescript/features/CompletionsProvider.ts
deleted file mode 100644
index da4a5cd54..000000000
--- a/tools/language-server/src/plugins/typescript/features/CompletionsProvider.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import type { CompletionContext, CompletionItem, Position, TextDocumentIdentifier, MarkupContent } from 'vscode-languageserver';
-import type { LanguageServiceManager } from '../LanguageServiceManager';
-import { isInsideFrontmatter } from '../../../core/documents/utils';
-import { Document } from '../../../core/documents';
-import * as ts from 'typescript';
-import { CompletionList, MarkupKind } from 'vscode-languageserver';
-import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
-import { scriptElementKindToCompletionItemKind, getCommitCharactersForScriptElement, toVirtualAstroFilePath } from '../utils';
-
-const completionOptions: ts.GetCompletionsAtPositionOptions = Object.freeze({
- importModuleSpecifierPreference: 'relative',
- importModuleSpecifierEnding: 'js',
- quotePreference: 'single',
-});
-
-export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDocumentIdentifier {
- position: Position;
-}
-
-export class CompletionsProviderImpl implements CompletionsProvider<CompletionEntryWithIdentifer> {
- constructor(private lang: LanguageServiceManager) {}
-
- async getCompletions(document: Document, position: Position, _completionContext?: CompletionContext): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> {
- // TODO: handle inside expression
- if (!isInsideFrontmatter(document.getText(), document.offsetAt(position))) {
- return null;
- }
-
- const filePath = document.getFilePath();
- if (!filePath) throw new Error();
-
- const { tsDoc, lang } = await this.lang.getTypeScriptDoc(document);
- const fragment = await tsDoc.getFragment();
-
- const offset = document.offsetAt(position);
-
- const entries = lang.getCompletionsAtPosition(fragment.filePath, offset, completionOptions)?.entries || [];
-
- const completionItems = entries
- .map((entry: ts.CompletionEntry) => this.toCompletionItem(fragment, entry, document.uri, position, new Set()))
- .filter((i) => i) as CompletionItem[];
-
- return CompletionList.create(completionItems, true);
- }
-
- async resolveCompletion(document: Document, completionItem: AppCompletionItem<CompletionEntryWithIdentifer>): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> {
- const { data: comp } = completionItem;
- const { tsDoc, lang } = await this.lang.getTypeScriptDoc(document);
-
- let filePath = toVirtualAstroFilePath(tsDoc.filePath);
-
- if (!comp || !filePath) {
- return completionItem;
- }
-
- const fragment = await tsDoc.getFragment();
- const detail = lang.getCompletionEntryDetails(
- filePath, // fileName
- fragment.offsetAt(comp.position), // position
- comp.name, // entryName
- {}, // formatOptions
- comp.source, // source
- {}, // preferences
- comp.data // data
- );
-
- if (detail) {
- const { detail: itemDetail, documentation: itemDocumentation } = this.getCompletionDocument(detail);
-
- completionItem.detail = itemDetail;
- completionItem.documentation = itemDocumentation;
- }
-
- return completionItem;
- }
-
- private toCompletionItem(
- fragment: any,
- comp: ts.CompletionEntry,
- uri: string,
- position: Position,
- existingImports: Set<string>
- ): AppCompletionItem<CompletionEntryWithIdentifer> | null {
- return {
- label: comp.name,
- insertText: comp.insertText,
- kind: scriptElementKindToCompletionItemKind(comp.kind),
- commitCharacters: getCommitCharactersForScriptElement(comp.kind),
- // Make sure svelte component takes precedence
- sortText: comp.sortText,
- preselect: comp.isRecommended,
- // pass essential data for resolving completion
- data: {
- ...comp,
- uri,
- position,
- },
- };
- }
-
- private getCompletionDocument(compDetail: ts.CompletionEntryDetails) {
- const { source, documentation: tsDocumentation, displayParts, tags } = compDetail;
- let detail: string = ts.displayPartsToString(displayParts);
-
- if (source) {
- const importPath = ts.displayPartsToString(source);
- detail = `Auto import from ${importPath}\n${detail}`;
- }
-
- const documentation: MarkupContent | undefined = tsDocumentation ? { value: tsDocumentation.join('\n'), kind: MarkupKind.Markdown } : undefined;
-
- return {
- documentation,
- detail,
- };
- }
-}
diff --git a/tools/language-server/src/plugins/typescript/features/HoverProvider.ts b/tools/language-server/src/plugins/typescript/features/HoverProvider.ts
deleted file mode 100644
index f772bc390..000000000
--- a/tools/language-server/src/plugins/typescript/features/HoverProvider.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { LanguageServiceManager } from '../LanguageServiceManager';
-import ts from 'typescript';
-import { Hover, Position } from 'vscode-languageserver';
-import { Document, mapObjWithRangeToOriginal } from '../../../core/documents';
-import { HoverProvider } from '../../interfaces';
-import { getMarkdownDocumentation } from '../previewer';
-import { convertRange, toVirtualAstroFilePath } from '../utils';
-
-export class HoverProviderImpl implements HoverProvider {
- constructor(private readonly lang: LanguageServiceManager) {}
-
- async doHover(document: Document, position: Position): Promise<Hover | null> {
- const { lang, tsDoc } = await this.getLSAndTSDoc(document);
- const fragment = await tsDoc.getFragment();
-
- const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
- const filePath = toVirtualAstroFilePath(tsDoc.filePath);
- let info = lang.getQuickInfoAtPosition(filePath, offset);
- if (!info) {
- return null;
- }
-
- const textSpan = info.textSpan;
-
- const declaration = ts.displayPartsToString(info.displayParts);
- const documentation = getMarkdownDocumentation(info.documentation, info.tags);
-
- // https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
- const contents = ['```typescript', declaration, '```'].concat(documentation ? ['---', documentation] : []).join('\n');
-
- return mapObjWithRangeToOriginal(fragment, {
- range: convertRange(fragment, textSpan),
- contents,
- });
- }
-
- private async getLSAndTSDoc(document: Document) {
- return this.lang.getTypeScriptDoc(document);
- }
-}
diff --git a/tools/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts b/tools/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts
deleted file mode 100644
index 93bad724f..000000000
--- a/tools/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import type { LanguageServiceManager } from '../LanguageServiceManager';
-import type { SignatureHelpProvider } from '../../interfaces';
-import ts from 'typescript';
-import {
- Position,
- SignatureHelpContext,
- SignatureHelp,
- SignatureHelpTriggerKind,
- SignatureInformation,
- ParameterInformation,
- MarkupKind,
- CancellationToken,
-} from 'vscode-languageserver';
-import { Document } from '../../../core/documents';
-import { getMarkdownDocumentation } from '../previewer';
-import { toVirtualAstroFilePath } from '../utils';
-
-export class SignatureHelpProviderImpl implements SignatureHelpProvider {
- constructor(private readonly lang: LanguageServiceManager) {}
-
- private static readonly triggerCharacters = ['(', ',', '<'];
- private static readonly retriggerCharacters = [')'];
-
- async getSignatureHelp(document: Document, position: Position, context: SignatureHelpContext | undefined, cancellationToken?: CancellationToken): Promise<SignatureHelp | null> {
- const { lang, tsDoc } = await this.lang.getTypeScriptDoc(document);
- const fragment = await tsDoc.getFragment();
-
- if (cancellationToken?.isCancellationRequested) {
- return null;
- }
-
- const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
- const triggerReason = this.toTsTriggerReason(context);
- const info = lang.getSignatureHelpItems(toVirtualAstroFilePath(tsDoc.filePath), offset, triggerReason ? { triggerReason } : undefined);
- if (!info || info.items.some((signature) => this.isInSvelte2tsxGeneratedFunction(signature))) {
- return null;
- }
-
- const signatures = info.items.map(this.toSignatureHelpInformation);
-
- return {
- signatures,
- activeSignature: info.selectedItemIndex,
- activeParameter: info.argumentIndex,
- };
- }
-
- private isReTrigger(isRetrigger: boolean, triggerCharacter: string): triggerCharacter is ts.SignatureHelpRetriggerCharacter {
- return isRetrigger && (this.isTriggerCharacter(triggerCharacter) || SignatureHelpProviderImpl.retriggerCharacters.includes(triggerCharacter));
- }
-
- private isTriggerCharacter(triggerCharacter: string): triggerCharacter is ts.SignatureHelpTriggerCharacter {
- return SignatureHelpProviderImpl.triggerCharacters.includes(triggerCharacter);
- }
-
- /**
- * adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L103
- */
- private toTsTriggerReason(context: SignatureHelpContext | undefined): ts.SignatureHelpTriggerReason {
- switch (context?.triggerKind) {
- case SignatureHelpTriggerKind.TriggerCharacter:
- if (context.triggerCharacter) {
- if (this.isReTrigger(context.isRetrigger, context.triggerCharacter)) {
- return { kind: 'retrigger', triggerCharacter: context.triggerCharacter };
- }
- if (this.isTriggerCharacter(context.triggerCharacter)) {
- return {
- kind: 'characterTyped',
- triggerCharacter: context.triggerCharacter,
- };
- }
- }
- return { kind: 'invoked' };
- case SignatureHelpTriggerKind.ContentChange:
- return context.isRetrigger ? { kind: 'retrigger' } : { kind: 'invoked' };
-
- case SignatureHelpTriggerKind.Invoked:
- default:
- return { kind: 'invoked' };
- }
- }
-
- /**
- * adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L73
- */
- private toSignatureHelpInformation(item: ts.SignatureHelpItem): SignatureInformation {
- const [prefixLabel, separatorLabel, suffixLabel] = [item.prefixDisplayParts, item.separatorDisplayParts, item.suffixDisplayParts].map(ts.displayPartsToString);
-
- let textIndex = prefixLabel.length;
- let signatureLabel = '';
- const parameters: ParameterInformation[] = [];
- const lastIndex = item.parameters.length - 1;
-
- item.parameters.forEach((parameter, index) => {
- const label = ts.displayPartsToString(parameter.displayParts);
-
- const startIndex = textIndex;
- const endIndex = textIndex + label.length;
- const doc = ts.displayPartsToString(parameter.documentation);
-
- signatureLabel += label;
- parameters.push(ParameterInformation.create([startIndex, endIndex], doc));
-
- if (index < lastIndex) {
- textIndex = endIndex + separatorLabel.length;
- signatureLabel += separatorLabel;
- }
- });
- const signatureDocumentation = getMarkdownDocumentation(
- item.documentation,
- item.tags.filter((tag) => tag.name !== 'param')
- );
-
- return {
- label: prefixLabel + signatureLabel + suffixLabel,
- documentation: signatureDocumentation
- ? {
- value: signatureDocumentation,
- kind: MarkupKind.Markdown,
- }
- : undefined,
- parameters,
- };
- }
-
- private isInSvelte2tsxGeneratedFunction(signatureHelpItem: ts.SignatureHelpItem) {
- return signatureHelpItem.prefixDisplayParts.some((part) => part.text.includes('__sveltets'));
- }
-}
diff --git a/tools/language-server/src/plugins/typescript/features/utils.ts b/tools/language-server/src/plugins/typescript/features/utils.ts
deleted file mode 100644
index 8c87dc5f4..000000000
--- a/tools/language-server/src/plugins/typescript/features/utils.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { SnapshotFragment, DocumentSnapshot } from '../DocumentSnapshot';
-import type { LanguageServiceManager } from '../LanguageServiceManager';
-
-/**
- * Checks if this a section that should be completely ignored
- * because it's purely generated.
- */
-export function isInGeneratedCode(text: string, start: number, end: number) {
- const lineStart = text.lastIndexOf('\n', start);
- const lineEnd = text.indexOf('\n', end);
- const lastStart = text.substring(lineStart, start).lastIndexOf('/*Ωignore_startΩ*/');
- const lastEnd = text.substring(lineStart, start).lastIndexOf('/*Ωignore_endΩ*/');
- return lastStart > lastEnd && text.substring(end, lineEnd).includes('/*Ωignore_endΩ*/');
-}
-
-/**
- * Checks that this isn't a text span that should be completely ignored
- * because it's purely generated.
- */
-export function isNoTextSpanInGeneratedCode(text: string, span: ts.TextSpan) {
- return !isInGeneratedCode(text, span.start, span.start + span.length);
-}
-
-export class SnapshotFragmentMap {
- private map = new Map<string, { fragment: SnapshotFragment; snapshot: DocumentSnapshot }>();
- constructor(private languageServiceManager: LanguageServiceManager) {}
-
- set(fileName: string, content: { fragment: SnapshotFragment; snapshot: DocumentSnapshot }) {
- this.map.set(fileName, content);
- }
-
- get(fileName: string) {
- return this.map.get(fileName);
- }
-
- getFragment(fileName: string) {
- return this.map.get(fileName)?.fragment;
- }
-
- async retrieve(fileName: string) {
- let snapshotFragment = this.get(fileName);
- if (!snapshotFragment) {
- const snapshot = await this.languageServiceManager.getSnapshot(fileName);
- const fragment = await snapshot.getFragment();
- snapshotFragment = { fragment, snapshot };
- this.set(fileName, snapshotFragment);
- }
- return snapshotFragment;
- }
-
- async retrieveFragment(fileName: string) {
- return (await this.retrieve(fileName)).fragment;
- }
-}
diff --git a/tools/language-server/src/plugins/typescript/languageService.ts b/tools/language-server/src/plugins/typescript/languageService.ts
deleted file mode 100644
index 22e2b1cdd..000000000
--- a/tools/language-server/src/plugins/typescript/languageService.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-/* eslint-disable require-jsdoc */
-
-import * as ts from 'typescript';
-import { basename } from 'path';
-import { ensureRealAstroFilePath, findTsConfigPath } from './utils';
-import { Document } from '../../core/documents';
-import { SnapshotManager } from './SnapshotManager';
-import { createDocumentSnapshot, DocumentSnapshot } from './DocumentSnapshot';
-import { createAstroModuleLoader } from './module-loader';
-
-const services = new Map<string, Promise<LanguageServiceContainer>>();
-
-export interface LanguageServiceContainer {
- readonly tsconfigPath: string;
- readonly snapshotManager: SnapshotManager;
- getService(): ts.LanguageService;
- updateDocument(documentOrFilePath: Document | string): ts.IScriptSnapshot;
- deleteDocument(filePath: string): void;
-}
-
-export interface LanguageServiceDocumentContext {
- getWorkspaceRoot(fileName: string): string;
- createDocument: (fileName: string, content: string) => Document;
-}
-
-export async function getLanguageService(path: string, workspaceUris: string[], docContext: LanguageServiceDocumentContext): Promise<LanguageServiceContainer> {
- const tsconfigPath = findTsConfigPath(path, workspaceUris);
- const workspaceRoot = docContext.getWorkspaceRoot(path);
-
- let service: LanguageServiceContainer;
- if (services.has(tsconfigPath)) {
- service = (await services.get(tsconfigPath)) as LanguageServiceContainer;
- } else {
- const newServicePromise = createLanguageService(tsconfigPath, workspaceRoot, docContext);
- services.set(tsconfigPath, newServicePromise);
- service = await newServicePromise;
- }
-
- return service;
-}
-
-export async function getLanguageServiceForDocument(document: Document, workspaceUris: string[], docContext: LanguageServiceDocumentContext): Promise<ts.LanguageService> {
- return getLanguageServiceForPath(document.getFilePath() || '', workspaceUris, docContext);
-}
-
-export async function getLanguageServiceForPath(path: string, workspaceUris: string[], docContext: LanguageServiceDocumentContext): Promise<ts.LanguageService> {
- return (await getLanguageService(path, workspaceUris, docContext)).getService();
-}
-
-async function createLanguageService(tsconfigPath: string, workspaceRoot: string, docContext: LanguageServiceDocumentContext): Promise<LanguageServiceContainer> {
- const parseConfigHost: ts.ParseConfigHost = {
- ...ts.sys,
- readDirectory: (path, extensions, exclude, include, depth) => {
- 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
- );
- }
-
- 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: ['src'],
- },
- workspaceRoot || process.cwd()
- );
-
- const astroModuleLoader = createAstroModuleLoader(getScriptSnapshot, {});
-
- const host: ts.LanguageServiceHost = {
- getNewLine: () => ts.sys.newLine,
- useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
- readFile: astroModuleLoader.readFile,
- writeFile: astroModuleLoader.writeFile,
- fileExists: astroModuleLoader.fileExists,
- directoryExists: astroModuleLoader.directoryExists,
- getDirectories: astroModuleLoader.getDirectories,
- readDirectory: astroModuleLoader.readDirectory,
- realpath: astroModuleLoader.realpath,
-
- getCompilationSettings: () => project.options,
- getCurrentDirectory: () => workspaceRoot,
- getDefaultLibFileName: () => ts.getDefaultLibFilePath(project.options),
-
- getProjectVersion: () => `${projectVersion}`,
- getScriptFileNames: () => Array.from(new Set([...snapshotManager.getFileNames(), ...snapshotManager.getProjectFileNames()])),
- getScriptSnapshot,
- getScriptVersion: (fileName: string) => getScriptSnapshot(fileName).version.toString(),
- };
-
- const languageService: ts.LanguageService = ts.createLanguageService(host);
- const languageServiceProxy = new Proxy(languageService, {
- get(target, prop) {
- return Reflect.get(target, prop);
- },
- });
-
- return {
- tsconfigPath,
- snapshotManager,
- getService: () => languageServiceProxy,
- updateDocument,
- deleteDocument,
- };
-
- function onProjectUpdated() {
- projectVersion++;
- }
-
- function deleteDocument(filePath: string) {
- snapshotManager.delete(filePath);
- }
-
- function updateDocument(documentOrFilePath: Document | string) {
- const filePath = ensureRealAstroFilePath(typeof documentOrFilePath === 'string' ? documentOrFilePath : documentOrFilePath.getFilePath() || '');
- const document = typeof documentOrFilePath === 'string' ? undefined : documentOrFilePath;
-
- if (!filePath) {
- throw new Error(`Unable to find document`);
- }
-
- const previousSnapshot = snapshotManager.get(filePath);
- if (document && previousSnapshot?.version.toString() === `${document.version}`) {
- return previousSnapshot;
- }
-
- const currentText = document ? document.getText() : null;
- const snapshot = createDocumentSnapshot(filePath, currentText, docContext.createDocument);
- snapshotManager.set(filePath, snapshot);
- onProjectUpdated();
- return snapshot;
- }
-
- function getScriptSnapshot(fileName: string): DocumentSnapshot {
- fileName = ensureRealAstroFilePath(fileName);
-
- let doc = snapshotManager.get(fileName);
- if (doc) {
- return doc;
- }
-
- doc = createDocumentSnapshot(fileName, null, docContext.createDocument);
- snapshotManager.set(fileName, doc);
- return doc;
- }
-}
-
-/**
- * This should only be used when there's no jsconfig/tsconfig at all
- */
-function getDefaultJsConfig(): {
- compilerOptions: ts.CompilerOptions;
- include: string[];
-} {
- return {
- compilerOptions: {
- maxNodeModuleJsDepth: 2,
- allowSyntheticDefaultImports: true,
- allowJs: true,
- },
- include: ['src'],
- };
-}
-
-function getDefaultExclude() {
- return ['dist', 'node_modules'];
-}
diff --git a/tools/language-server/src/plugins/typescript/module-loader.ts b/tools/language-server/src/plugins/typescript/module-loader.ts
deleted file mode 100644
index 2bcb206e7..000000000
--- a/tools/language-server/src/plugins/typescript/module-loader.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import ts from 'typescript';
-import type { DocumentSnapshot } from './SnapshotManager';
-import { isVirtualAstroFilePath, ensureRealAstroFilePath, getExtensionFromScriptKind } from './utils';
-import { createAstroSys } from './astro-sys';
-
-/**
- * Caches resolved modules.
- */
-class ModuleResolutionCache {
- private cache = new Map<string, ts.ResolvedModule>();
-
- /**
- * Tries to get a cached module.
- */
- get(moduleName: string, containingFile: string): ts.ResolvedModule | undefined {
- return this.cache.get(this.getKey(moduleName, containingFile));
- }
-
- /**
- * Caches resolved module, if it is not undefined.
- */
- set(moduleName: string, containingFile: string, resolvedModule: ts.ResolvedModule | undefined) {
- if (!resolvedModule) {
- return;
- }
- this.cache.set(this.getKey(moduleName, containingFile), resolvedModule);
- }
-
- /**
- * Deletes module from cache. Call this if a file was deleted.
- * @param resolvedModuleName full path of the module
- */
- delete(resolvedModuleName: string): void {
- this.cache.forEach((val, key) => {
- if (val.resolvedFileName === resolvedModuleName) {
- this.cache.delete(key);
- }
- });
- }
-
- private getKey(moduleName: string, containingFile: string) {
- return containingFile + ':::' + ensureRealAstroFilePath(moduleName);
- }
-}
-
-/**
- * Creates a module loader specifically for `.astro` files.
- *
- * The typescript language service tries to look up other files that are referenced in the currently open astro file.
- * For `.ts`/`.js` files this works, for `.astro` files it does not by default.
- * Reason: The typescript language service does not know about the `.astro` file ending,
- * so it assumes it's a normal typescript file and searches for files like `../Component.astro.ts`, which is wrong.
- * In order to fix this, we need to wrap typescript's module resolution and reroute all `.astro.ts` file lookups to .astro.
- *
- * @param getSnapshot A function which returns a (in case of astro file fully preprocessed) typescript/javascript snapshot
- * @param compilerOptions The typescript compiler options
- */
-export function createAstroModuleLoader(getSnapshot: (fileName: string) => DocumentSnapshot, compilerOptions: ts.CompilerOptions) {
- const astroSys = createAstroSys(getSnapshot);
- const moduleCache = new ModuleResolutionCache();
-
- return {
- fileExists: astroSys.fileExists,
- readFile: astroSys.readFile,
- writeFile: astroSys.writeFile,
- readDirectory: astroSys.readDirectory,
- directoryExists: astroSys.directoryExists,
- getDirectories: astroSys.getDirectories,
- realpath: astroSys.realpath,
- deleteFromModuleCache: (path: string) => moduleCache.delete(path),
- resolveModuleNames,
- };
-
- function resolveModuleNames(moduleNames: string[], containingFile: string): Array<ts.ResolvedModule | undefined> {
- return moduleNames.map((moduleName) => {
- const cachedModule = moduleCache.get(moduleName, containingFile);
- if (cachedModule) {
- return cachedModule;
- }
-
- const resolvedModule = resolveModuleName(moduleName, containingFile);
- moduleCache.set(moduleName, containingFile, resolvedModule);
- return resolvedModule;
- });
- }
-
- function resolveModuleName(name: string, containingFile: string): ts.ResolvedModule | undefined {
- // Delegate to the TS resolver first.
- // If that does not bring up anything, try the Astro Module loader
- // which is able to deal with .astro files.
- const tsResolvedModule = ts.resolveModuleName(name, containingFile, compilerOptions, ts.sys).resolvedModule;
- if (tsResolvedModule && !isVirtualAstroFilePath(tsResolvedModule.resolvedFileName)) {
- return tsResolvedModule;
- }
-
- const astroResolvedModule = ts.resolveModuleName(name, containingFile, compilerOptions, astroSys).resolvedModule;
- if (!astroResolvedModule || !isVirtualAstroFilePath(astroResolvedModule.resolvedFileName)) {
- return astroResolvedModule;
- }
-
- const resolvedFileName = ensureRealAstroFilePath(astroResolvedModule.resolvedFileName);
- const snapshot = getSnapshot(resolvedFileName);
-
- const resolvedastroModule: ts.ResolvedModuleFull = {
- extension: getExtensionFromScriptKind(snapshot && snapshot.scriptKind),
- resolvedFileName,
- };
- return resolvedastroModule;
- }
-}
diff --git a/tools/language-server/src/plugins/typescript/previewer.ts b/tools/language-server/src/plugins/typescript/previewer.ts
deleted file mode 100644
index 710da4c17..000000000
--- a/tools/language-server/src/plugins/typescript/previewer.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-/**
- * adopted from https://github.com/microsoft/vscode/blob/10722887b8629f90cc38ee7d90d54e8246dc895f/extensions/typescript-language-features/src/utils/previewer.ts
- */
-
-import ts from 'typescript';
-import { isNotNullOrUndefined } from '../../utils';
-
-function replaceLinks(text: string): string {
- return (
- text
- // Http(s) links
- .replace(/\{@(link|linkplain|linkcode) (https?:\/\/[^ |}]+?)(?:[| ]([^{}\n]+?))?\}/gi, (_, tag: string, link: string, text?: string) => {
- switch (tag) {
- case 'linkcode':
- return `[\`${text ? text.trim() : link}\`](${link})`;
-
- default:
- return `[${text ? text.trim() : link}](${link})`;
- }
- })
- );
-}
-
-function processInlineTags(text: string): string {
- return replaceLinks(text);
-}
-
-function getTagBodyText(tag: ts.JSDocTagInfo): string | undefined {
- if (!tag.text) {
- return undefined;
- }
-
- // Convert to markdown code block if it is not already one
- function makeCodeblock(text: string): string {
- if (text.match(/^\s*[~`]{3}/g)) {
- return text;
- }
- return '```\n' + text + '\n```';
- }
-
- function makeExampleTag(text: string) {
- // check for caption tags, fix for https://github.com/microsoft/vscode/issues/79704
- const captionTagMatches = text.match(/<caption>(.*?)<\/caption>\s*(\r\n|\n)/);
- if (captionTagMatches && captionTagMatches.index === 0) {
- return captionTagMatches[1] + '\n\n' + makeCodeblock(text.substr(captionTagMatches[0].length));
- } else {
- return makeCodeblock(text);
- }
- }
-
- function makeEmailTag(text: string) {
- // fix obsucated email address, https://github.com/microsoft/vscode/issues/80898
- const emailMatch = text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/);
-
- if (emailMatch === null) {
- return text;
- } else {
- return `${emailMatch[1]} ${emailMatch[2]}`;
- }
- }
-
- switch (tag.name) {
- case 'example':
- return makeExampleTag(ts.displayPartsToString(tag.text));
- case 'author':
- return makeEmailTag(ts.displayPartsToString(tag.text));
- case 'default':
- return makeCodeblock(ts.displayPartsToString(tag.text));
- }
-
- return processInlineTags(ts.displayPartsToString(tag.text));
-}
-
-export function getTagDocumentation(tag: ts.JSDocTagInfo): string | undefined {
- function getWithType() {
- const body = (ts.displayPartsToString(tag.text) || '').split(/^(\S+)\s*-?\s*/);
- if (body?.length === 3) {
- const param = body[1];
- const doc = body[2];
- const label = `*@${tag.name}* \`${param}\``;
- if (!doc) {
- return label;
- }
- return label + (doc.match(/\r\n|\n/g) ? ' \n' + processInlineTags(doc) : ` — ${processInlineTags(doc)}`);
- }
- }
-
- switch (tag.name) {
- case 'augments':
- case 'extends':
- case 'param':
- case 'template':
- return getWithType();
- }
-
- // Generic tag
- const label = `*@${tag.name}*`;
- const text = getTagBodyText(tag);
- if (!text) {
- return label;
- }
- return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`);
-}
-
-export function plain(parts: ts.SymbolDisplayPart[] | string): string {
- return processInlineTags(typeof parts === 'string' ? parts : ts.displayPartsToString(parts));
-}
-
-export function getMarkdownDocumentation(documentation: ts.SymbolDisplayPart[] | undefined, tags: ts.JSDocTagInfo[] | undefined) {
- let result: Array<string | undefined> = [];
- if (documentation) {
- result.push(plain(documentation));
- }
-
- if (tags) {
- result = result.concat(tags.map(getTagDocumentation));
- }
-
- return result.filter(isNotNullOrUndefined).join('\n\n');
-}
diff --git a/tools/language-server/src/plugins/typescript/utils.ts b/tools/language-server/src/plugins/typescript/utils.ts
deleted file mode 100644
index a1a748946..000000000
--- a/tools/language-server/src/plugins/typescript/utils.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-import * as ts from 'typescript';
-import { CompletionItemKind, DiagnosticSeverity, Position, Range } from 'vscode-languageserver';
-import { dirname } from 'path';
-import { pathToUrl } from '../../utils';
-import { mapRangeToOriginal } from '../../core/documents';
-import { SnapshotFragment } from './DocumentSnapshot';
-
-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 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;
-}
-
-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;
- }
-}
-
-export function getExtensionFromScriptKind(kind: ts.ScriptKind | undefined): ts.Extension {
- switch (kind) {
- case ts.ScriptKind.JSX:
- return ts.Extension.Jsx;
- case ts.ScriptKind.TS:
- return ts.Extension.Ts;
- case ts.ScriptKind.TSX:
- return ts.Extension.Tsx;
- case ts.ScriptKind.JSON:
- return ts.Extension.Json;
- case ts.ScriptKind.JS:
- default:
- return ts.Extension.Js;
- }
-}
-
-export function convertRange(document: { positionAt: (offset: number) => Position }, range: { start?: number; length?: number }): Range {
- return Range.create(document.positionAt(range.start || 0), document.positionAt((range.start || 0) + (range.length || 0)));
-}
-
-export function convertToLocationRange(defDoc: SnapshotFragment, textSpan: ts.TextSpan): Range {
- const range = mapRangeToOriginal(defDoc, convertRange(defDoc, textSpan));
- // Some definition like the svelte component class definition don't exist in the original, so we map to 0,1
- if (range.start.line < 0) {
- range.start.line = 0;
- range.start.character = 1;
- }
- if (range.end.line < 0) {
- range.end = range.start;
- }
-
- return range;
-}
-
-type FrameworkExt = 'astro' | 'vue' | 'jsx' | 'tsx' | 'svelte';
-
-export function isVirtualFrameworkFilePath(ext: FrameworkExt, filePath: string) {
- return filePath.endsWith('.' + ext + '.ts');
-}
-
-export function isAstroFilePath(filePath: string) {
- return filePath.endsWith('.astro');
-}
-
-export function isVirtualAstroFilePath(filePath: string) {
- return isVirtualFrameworkFilePath('astro', filePath);
-}
-
-export function isVirtualVueFilePath(filePath: string) {
- return isVirtualFrameworkFilePath('vue', filePath);
-}
-
-export function isVirtualJsxFilePath(filePath: string) {
- return isVirtualFrameworkFilePath('jsx', filePath) || isVirtualFrameworkFilePath('tsx', filePath);
-}
-
-export function isVirtualSvelteFilePath(filePath: string) {
- return isVirtualFrameworkFilePath('svelte', filePath);
-}
-
-export function isVirtualFilePath(filePath: string) {
- return isVirtualAstroFilePath(filePath) || isVirtualVueFilePath(filePath) || isVirtualSvelteFilePath(filePath) || isVirtualJsxFilePath(filePath);
-}
-
-export function toVirtualAstroFilePath(filePath: string) {
- if (isVirtualFrameworkFilePath('astro', filePath)) {
- return filePath;
- }
- return `${filePath}.ts`;
-}
-
-export function toRealAstroFilePath(filePath: string) {
- return filePath.slice(0, -'.ts'.length);
-}
-
-export function ensureRealAstroFilePath(filePath: string) {
- return isVirtualAstroFilePath(filePath) ? toRealAstroFilePath(filePath) : filePath;
-}
-
-export function ensureRealFilePath(filePath: string) {
- return isVirtualFilePath(filePath) ? filePath.slice(0, filePath.length - 3) : 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 : '';
-}
-
-/** */
-export function isSubPath(uri: string, possibleSubPath: string): boolean {
- 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;
-}
-
-function append(result: string, str: string, n: number): string {
- while (n > 0) {
- if (n & 1) {
- result += str;
- }
- n >>= 1;
- str += str;
- }
- return result;
-}
diff --git a/tools/language-server/src/types/index.d.ts b/tools/language-server/src/types/index.d.ts
deleted file mode 100644
index e048b1a0c..000000000
--- a/tools/language-server/src/types/index.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * Starts `astro-languageservice`
- */
-export function startServer(): void {}
diff --git a/tools/language-server/src/utils.ts b/tools/language-server/src/utils.ts
deleted file mode 100644
index ba3d9366e..000000000
--- a/tools/language-server/src/utils.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { URI } from 'vscode-uri';
-import { Position, Range } from 'vscode-languageserver';
-import { Node } from 'vscode-html-languageservice';
-
-/** Normalizes a document URI */
-export function normalizeUri(uri: string): string {
- 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, '/');
-}
-
-/** Converts a path to a URL */
-export function pathToUrl(path: string) {
- 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.
- */
-export function isPossibleComponent(node: Node): boolean {
- 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.
- */
-export function isPossibleClientComponent(node: Node): boolean {
- 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], []);
-}
-
-/** 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));
-}
-
-export function isNotNullOrUndefined<T>(val: T | undefined | null): val is T {
- return val !== undefined && val !== null;
-}
-
-/** Checks if a position is inside range */
-export function isInRange(positionToTest: Position, range: Range): boolean {
- 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);
-}
-
-/**
- * Debounces a function but cancels previous invocation only if
- * a second function determines it should.
- *
- * @param fn The function with it's argument
- * @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;
-
- return (arg: T) => {
- if (shouldCancelPrevious(arg, prevArg)) {
- clearTimeout(timeout);
- }
-
- prevArg = arg;
- timeout = setTimeout(() => {
- fn(arg);
- prevArg = undefined;
- }, miliseconds);
- };
-}