Remove VSCode and Langauge Server from this monorepo (#1230)

* Remove VSCode and Langauge Server from this monorepo

* Adds back in the syntax files
This commit is contained in:
Matthew Phillips 2021-08-26 13:14:47 -04:00 committed by GitHub
parent c83d481733
commit c4cfc0d5fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 0 additions and 5004 deletions

View file

@ -13,12 +13,10 @@
"build:one": "lerna run build --scope",
"build:all": "lerna run build --scope \"{astro,@astrojs/*}\"",
"build:core": "lerna run build --scope \"{astro,@astrojs/parser,@astrojs/markdown-support}\"",
"build:vscode": "lerna run build --scope \"{@astrojs/language-server,astro-vscode,@astrojs/parser}\"",
"dev": "yarn dev:core --parallel --stream",
"dev:one": "lerna run dev --scope --parallel --stream",
"dev:all": "lerna run dev --scope \"{astro,@astrojs/*}\" --parallel --stream",
"dev:core": "lerna run dev --scope \"{astro,@astrojs/parser,@astrojs/markdown-support}\" --parallel --stream",
"dev:vscode": "lerna run dev --scope \"{@astrojs/language-server,astro-vscode,@astrojs/parser}\" --parallel --stream",
"format": "prettier -w .",
"lint": "eslint \"packages/**/*.ts\"",
"test": "yarn workspace astro run test",
@ -29,7 +27,6 @@
"packages/*",
"examples/*",
"scripts",
"tools/*",
"www",
"docs",
"packages/astro/test/fixtures/builtins/packages/*",

View file

@ -1,27 +0,0 @@
# @astrojs/language-server
## 0.6.0
- Fixes bug with signature help not appear in the component script section.
- Adds completion suggestions for Astro.\* APIs in the component script.
- Adds support for Hover based hints in the component script section.
- Fixes bug with Go to Definition (cmd + click) of Components.
## 0.5.0
- Fix `bin` file
## 0.5.0-next.1
- Expose `bin/server.js` as `astro-ls`
## 0.5.0-next.0
- Moved to scoped `@astrojs/language-server` package
- Removed some `devDependencies` from the bundle and added them to `dependencies`
## 0.4.0
### Minor Changes
- 06e2597: Adds support for import suggestions

View file

@ -1,37 +0,0 @@
MIT License
Copyright (c) 2021 Nate Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
This license applies to code originating from the https://github.com/sveltejs/language-tools repository,
which has provided an extremely solid foundation for us to build upon:
Copyright (c) 2020-Present [these people](https://github.com/sveltejs/language-tools/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

View file

@ -1,27 +0,0 @@
type AstroRenderedHTML = string;
type FetchContentResult<ContentFrontmatter extends Record<string, any> = Record<string, any>> = {
astro: {
headers: string[];
source: string;
html: AstroRenderedHTML;
};
url: URL;
} & ContentFrontmatter;
interface AstroPageRequest {
url: URL;
canonicalURL: URL;
}
interface Astro {
isPage: boolean;
fetchContent<ContentFrontmatter>(globStr: string): FetchContentResult<ContentFrontmatter>[];
props: Record<string, number | string | any>;
request: AstroPageRequest;
site: URL;
}
declare const Astro: Astro;
export default function (): string;

View file

@ -1,7 +0,0 @@
#! /usr/bin/env node
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { startServer } = require('@astrojs/language-server/dist/index.js');
startServer();

View file

@ -1,35 +0,0 @@
{
"name": "@astrojs/language-server",
"version": "0.6.0",
"author": "Skypack",
"license": "MIT",
"type": "commonjs",
"main": "dist/index.js",
"types": "types/index.d.ts",
"bin": {
"astro-ls": "./bin/server.js"
},
"files": [
"dist",
"astro.d.ts",
"bin",
"types"
],
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\""
},
"dependencies": {
"source-map": "^0.7.3",
"typescript": "^4.3.1-rc",
"vscode-css-languageservice": "^5.1.1",
"vscode-emmet-helper": "2.1.2",
"vscode-html-languageservice": "^3.0.3",
"vscode-languageserver": "6.1.1",
"vscode-languageserver-textdocument": "^1.0.1"
},
"devDependencies": {
"astro": "0.20.0",
"astro-scripts": "0.0.1"
}
}

View file

@ -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;
}
}

View file

@ -1 +0,0 @@
export * from './ConfigManager';

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -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));
}

View file

@ -1,5 +0,0 @@
export * from './Document';
export * from './DocumentBase';
export * from './DocumentManager';
export * from './DocumentMapper';
export * from './utils';

View file

@ -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 };
}
}
}

View file

@ -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;
}

View file

@ -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];
}

View file

@ -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();
}

View file

@ -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;
}
}
}

View file

@ -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);
}

View file

@ -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';
}
}

View file

@ -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\//, '');
}

View file

@ -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();
}
}

View file

@ -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));
}
}

View file

@ -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];
}

View file

@ -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);
}
}

View file

@ -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';

View file

@ -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>;

View file

@ -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++;
}
}

View file

@ -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);
}
}

View file

@ -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}`
);
}
}
}

View file

@ -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;
}
}
}
}
}
}

View file

@ -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;
}

View file

@ -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,
};
}
}

View file

@ -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);
}
}

View file

@ -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'));
}
}

View file

@ -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;
}
}

View file

@ -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'];
}

View file

@ -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;
}
}

View file

@ -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');
}

View file

@ -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;
}

View file

@ -1,4 +0,0 @@
/**
* Starts `astro-languageservice`
*/
export function startServer(): void {}

View file

@ -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);
};
}

View file

@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"target": "ES2020"
},
"include": ["src"],
"exclude": ["node_modules"]
}

View file

@ -1,4 +0,0 @@
src/
tsconfig.base.json
tsconfig.json
CONTRIBUTING.md

View file

@ -1,46 +0,0 @@
# astro-vscode
## 0.6.1
- Makes the v0.6.0 features actually work 😅
## 0.6.0
- Fixes bug with signature help not appearing in the component script section.
- Adds completion suggestions for `Astro.*` APIs in the component script.
- Adds support for Hover based hints in the component script section.
- Fixes bug with Go to Definition (cmd + click) for Components.
## 0.5.0
- Bug fixes, dependency updates
## 0.4.3
### Patch Changes
- Improve support for <Markdown> component
- Bug fixes and improvements
## 0.4.2
### Patch Changes
- b3886c2: Added support for new <Markdown> component
## 0.4.1
### Patch Changes
- Updated VS Code Marketplace banner
## 0.4.0
### Minor Changes
- 06e2597: Adds support for import suggestions
### Patch Changes
- Updated dependencies [06e2597]
- astro-languageserver@0.4.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View file

@ -1,20 +0,0 @@
# Contributing
## Development workflow
In the monorepo first install and build Astro:
```shell
yarn install
yarn build
```
To start the development server run:
```shell
yarn dev:vscode
```
Then in the **Debug** panel select **Launch Extension** from the dropdown and click the run button.
<img width="558" alt="Screen Shot 2021-05-07 at 8 51 37 AM" src="https://user-images.githubusercontent.com/361671/117452223-807e5580-af11-11eb-8404-dd615784408a.png">

View file

@ -1,36 +0,0 @@
{
"comments": {
"blockComment": ["<!--", "-->"]
},
"brackets": [
["---", "---"],
["<!--", "-->"],
["<", ">"],
["{", "}"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'" },
{ "open": "\"", "close": "\"" },
{ "open": "<!--", "close": "-->", "notIn": ["comment", "string"] },
{ "open": "/**", "close": " */", "notIn": ["string"] }
],
"autoCloseBefore": ";:.,=}])>` \n\t",
"surroundingPairs": [
{ "open": "'", "close": "'" },
{ "open": "\"", "close": "\"" },
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "<", "close": ">" }
],
"folding": {
"markers": {
"start": "^\\s*<!--\\s*#region\\b.*-->",
"end": "^\\s*<!--\\s*#endregion\\b.*-->"
}
}
}

View file

@ -1,43 +0,0 @@
{
"comments": {
"blockComment": ["<!--", "-->"]
},
"brackets": [
["---", "---"],
["<!--", "-->"],
["<", ">"],
["{", "}"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'" },
{ "open": "\"", "close": "\"" },
{
"open": "<",
"close": ">",
"notIn": ["string"]
},
{ "open": "<!--", "close": "-->", "notIn": ["comment", "string"] }
],
"autoCloseBefore": ";:.,=}])>` \n\t",
"surroundingPairs": [
{ "open": "'", "close": "'" },
{ "open": "\"", "close": "\"" },
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "<", "close": ">" },
{ "open": "`", "close": "`" },
{ "open": "_", "close": "_" },
{ "open": "*", "close": "*" }
],
"folding": {
"markers": {
"start": "^\\s*<!--\\s*#region\\b.*-->",
"end": "^\\s*<!--\\s*#endregion\\b.*-->"
}
}
}

View file

@ -1,161 +0,0 @@
{
"name": "astro-vscode",
"displayName": "Astro",
"description": "Language support for Astro",
"icon": "assets/icon.png",
"type": "commonjs",
"galleryBanner": {
"color": "#FFBE2D",
"theme": "dark"
},
"version": "0.6.1",
"author": "Astro",
"license": "MIT",
"publisher": "astro-build",
"private": true,
"scripts": {
"vscode:prepublish": "yarn build",
"vscode:publish": "node ./scripts/publish.mjs",
"build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json",
"dev": "astro-scripts dev \"src/**/*.ts\""
},
"engines": {
"vscode": "^1.52.0"
},
"activationEvents": [
"onLanguage:astro"
],
"dependencies": {
"@astrojs/language-server": "0.6.0",
"vscode-emmet-helper": "2.1.2",
"vscode-html-languageservice": "^3.0.3",
"vscode-languageclient": "~7.0.0"
},
"devDependencies": {
"@types/vscode": "^1.52.0",
"astro-scripts": "file:../../scripts",
"execa": "^5.0.0"
},
"main": "./dist/index.js",
"files": [
"dist/",
"languages/",
"syntaxes/"
],
"repository": {
"type": "git",
"directory": "vscode",
"url": "https://github.com/snowpackjs/astro"
},
"contributes": {
"configuration": {
"type": "object",
"title": "Astro configuration",
"properties": {
"astro.trace.server": {
"scope": "window",
"type": "string",
"enum": [
"off",
"messages",
"verbose"
],
"default": "off",
"description": "Traces the communication between VS Code and the language server."
}
}
},
"languages": [
{
"id": "astro",
"extensions": [
".astro"
],
"aliases": [
"Astro"
],
"configuration": "./languages/astro-language-configuration.json"
},
{
"id": "astro-markdown",
"aliases": [
"Astro Markdown"
],
"configuration": "./languages/astro-markdown-language-configuration.json"
}
],
"grammars": [
{
"language": "astro",
"scopeName": "text.html.astro",
"path": "./syntaxes/astro.tmLanguage.json",
"embeddedLanguages": {
"text.html.astro": "astro",
"text.html.markdown.astro": "astro-markdown",
"text.html": "html",
"source.css": "css",
"source.scss": "scss",
"source.sass": "sass",
"source.tsx": "typescriptreact",
"meta.embedded.block.frontmatter": "typescriptreact"
}
},
{
"language": "astro-markdown",
"scopeName": "text.html.markdown.astro",
"path": "./syntaxes/astro-markdown.tmLanguage.json",
"injectTo": [
"text.html.astro"
],
"embeddedLanguages": {
"text.html.astro": "astro",
"text.html.markdown.astro": "astro-markdown",
"text.html.markdown": "markdown",
"source.tsx": "typescriptreact",
"source.js": "javascript",
"source.css": "css",
"meta.embedded.block.css": "css",
"meta.embedded.block.astro": "astro",
"meta.embedded.block.ini": "ini",
"meta.embedded.block.java": "java",
"meta.embedded.block.lua": "lua",
"meta.embedded.block.makefile": "makefile",
"meta.embedded.block.perl": "perl",
"meta.embedded.block.r": "r",
"meta.embedded.block.ruby": "ruby",
"meta.embedded.block.php": "php",
"meta.embedded.block.sql": "sql",
"meta.embedded.block.vs_net": "vs_net",
"meta.embedded.block.xml": "xml",
"meta.embedded.block.xsl": "xsl",
"meta.embedded.block.yaml": "yaml",
"meta.embedded.block.dosbatch": "dosbatch",
"meta.embedded.block.clojure": "clojure",
"meta.embedded.block.coffee": "coffee",
"meta.embedded.block.c": "c",
"meta.embedded.block.cpp": "cpp",
"meta.embedded.block.diff": "diff",
"meta.embedded.block.dockerfile": "dockerfile",
"meta.embedded.block.go": "go",
"meta.embedded.block.groovy": "groovy",
"meta.embedded.block.pug": "jade",
"meta.embedded.block.javascript": "javascript",
"meta.embedded.block.json": "json",
"meta.embedded.block.less": "less",
"meta.embedded.block.objc": "objc",
"meta.embedded.block.scss": "scss",
"meta.embedded.block.perl6": "perl6",
"meta.embedded.block.powershell": "powershell",
"meta.embedded.block.python": "python",
"meta.embedded.block.rust": "rust",
"meta.embedded.block.scala": "scala",
"meta.embedded.block.shellscript": "shellscript",
"meta.embedded.block.typescript": "typescript",
"meta.embedded.block.typescriptreact": "typescriptreact",
"meta.embedded.block.csharp": "csharp",
"meta.embedded.block.fsharp": "fsharp"
}
}
]
}
}

View file

@ -1,41 +0,0 @@
import { promises as fs } from 'fs';
import { fileURLToPath } from 'url';
import execa from 'execa';
/** Copies `astro-languageserver` to our file */
async function publish() {
const p0 = execa('yarn', ['lerna', 'run', 'build', '--scope', 'astro-vscode', '--scope', '@astrojs/language-server'], { all: true });
p0.all.setEncoding('utf8');
for await (const chunk of p0.all) {
console.log(chunk);
if (/lerna success/g.test(chunk)) {
break;
}
if (/ERROR/g.test(chunk)) {
process.exit(1);
}
}
execa('npm', ['install'], { all: true });
const p1 = execa('vsce', ['publish'], { all: true });
p1.all.setEncoding('utf8');
for await (const chunk of p1.all) {
console.log(chunk);
if (/DONE/g.test(chunk)) {
break;
}
if (/ERROR/g.test(chunk)) {
process.exit(1);
}
}
p1.kill();
}
publish();

View file

@ -1,89 +0,0 @@
// Original source: https://github.com/Microsoft/vscode/blob/master/extensions/html-language-features/client/src/tagClosing.ts
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { window, workspace, Disposable, TextDocument, Position, SnippetString } from 'vscode';
import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol';
/** */
export function activateTagClosing(
tagProvider: (document: TextDocument, position: Position) => Thenable<string>,
supportedLanguages: { [id: string]: boolean },
configName: string
): Disposable {
const disposables: Disposable[] = [];
workspace.onDidChangeTextDocument((event) => onDidChangeTextDocument(event.document, event.contentChanges), null, disposables);
let isEnabled = false;
updateEnabledState();
window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables);
let timeout: NodeJS.Timer | undefined = void 0;
/** Check if this feature is enabled */
function updateEnabledState() {
isEnabled = false;
const editor = window.activeTextEditor;
if (!editor) {
return;
}
const document = editor.document;
if (!supportedLanguages[document.languageId]) {
return;
}
if (!workspace.getConfiguration(void 0, document.uri).get<boolean>(configName)) {
return;
}
isEnabled = true;
}
/** Handle text document changes */
function onDidChangeTextDocument(document: TextDocument, changes: readonly TextDocumentContentChangeEvent[]) {
if (!isEnabled) {
return;
}
const activeDocument = window.activeTextEditor && window.activeTextEditor.document;
if (document !== activeDocument || changes.length === 0) {
return;
}
if (typeof timeout !== 'undefined') {
clearTimeout(timeout);
}
const lastChange = changes[changes.length - 1];
const lastCharacter = lastChange.text[lastChange.text.length - 1];
if (('range' in lastChange && (lastChange.rangeLength ?? 0) > 0) || (lastCharacter !== '>' && lastCharacter !== '/')) {
return;
}
const rangeStart = 'range' in lastChange ? lastChange.range.start : new Position(0, document.getText().length);
const version = document.version;
timeout = setTimeout(() => {
const position = new Position(rangeStart.line, rangeStart.character + lastChange.text.length);
tagProvider(document, position).then((text) => {
if (text && isEnabled) {
const activeEditor = window.activeTextEditor;
if (activeEditor) {
const activeDocument = activeEditor.document;
if (document === activeDocument && activeDocument.version === version) {
const selections = activeEditor.selections;
if (selections.length && selections.some((s) => s.active.isEqual(position))) {
activeEditor.insertSnippet(
new SnippetString(text),
selections.map((s) => s.active)
);
} else {
activeEditor.insertSnippet(new SnippetString(text), position);
}
}
}
}
});
timeout = void 0;
}, 100);
}
return Disposable.from(...disposables);
}

View file

@ -1,70 +0,0 @@
import * as vscode from 'vscode';
import * as lsp from 'vscode-languageclient/node.js';
import { activateTagClosing } from './html/autoClose.js';
let docClient: lsp.LanguageClient;
const TagCloseRequest: lsp.RequestType<lsp.TextDocumentPositionParams, string, any> = new lsp.RequestType('html/tag');
/** */
export async function activate(context: vscode.ExtensionContext) {
docClient = createLanguageService(context, 'doc', 'astro', 'Astro', 6040);
await docClient.onReady();
}
/** */
function createLanguageService(context: vscode.ExtensionContext, mode: 'doc', id: string, name: string, port: number) {
const { workspace } = vscode;
const serverModule = require.resolve('@astrojs/language-server/bin/server.js');
const debugOptions = { execArgv: ['--nolazy', '--inspect=' + port] };
const serverOptions: lsp.ServerOptions = {
run: { module: serverModule, transport: lsp.TransportKind.ipc },
debug: {
module: serverModule,
transport: lsp.TransportKind.ipc,
options: debugOptions,
},
};
const serverInitOptions: any = {
mode: mode,
appRoot: vscode.env.appRoot,
language: vscode.env.language,
};
const clientOptions: lsp.LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'astro' }],
synchronize: {
configurationSection: ['javascript', 'typescript', 'prettier'],
fileEvents: workspace.createFileSystemWatcher('{**/*.js,**/*.ts}', false, false, false),
},
initializationOptions: {
...serverInitOptions,
configuration: {
prettier: workspace.getConfiguration('prettier'),
emmet: workspace.getConfiguration('emmet'),
typescript: workspace.getConfiguration('typescript'),
javascript: workspace.getConfiguration('javascript'),
},
dontFilterIncompleteCompletions: true, // VSCode filters client side and is smarter at it than us
},
};
const client = new lsp.LanguageClient(id, name, serverOptions, clientOptions);
context.subscriptions.push(client.start());
client
.onReady()
.then(() => {
const tagRequestor = (document: vscode.TextDocument, position: vscode.Position) => {
const param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
return client.sendRequest(TagCloseRequest, param);
};
const disposable = activateTagClosing(tagRequestor, { astro: true }, 'html.autoClosingTags');
context.subscriptions.push(disposable);
})
.catch((err) => {
console.error('Astro, unable to load language server.', err);
});
return client;
}

View file

@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "es2019",
"lib": ["ESNext"],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"composite": true,
"declaration": true,
"strict": true,
"skipLibCheck": true,
"baseUrl": "./",
"paths": {
"@astro-vscode/*": ["packages/*/src"]
}
}
}

View file

@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules"]
}