diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 0fdce55d2..ad1a0c1d9 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -54,6 +54,7 @@ "prettier-plugin-svelte": "1.1.0", "source-map": "^0.7.3", "svelte": "3.19.2", + "svelte2tsx": "~0.1.4", "typescript": "*", "vscode-css-languageservice": "4.1.0", "vscode-emmet-helper": "1.2.17", diff --git a/packages/language-server/src/lib/documents/utils.ts b/packages/language-server/src/lib/documents/utils.ts index 38994bcf5..e11700444 100644 --- a/packages/language-server/src/lib/documents/utils.ts +++ b/packages/language-server/src/lib/documents/utils.ts @@ -1,6 +1,16 @@ import { clamp } from '../../utils'; import { Position } from 'vscode-languageserver'; +export interface TagInformation { + content: string; + attributes: Record; + start: number; + end: number; + startPos: Position; + endPos: Position; + container: { start: number; end: number }; +} + function parseAttributeValue(value: string): string { return /^['"]/.test(value) ? value.slice(1, -1) : value; } @@ -9,7 +19,7 @@ function parseAttributes(str: string): Record { const attrs: Record = {}; str.split(/\s+/) .filter(Boolean) - .forEach(attr => { + .forEach((attr) => { const [name, value] = attr.split('='); attrs[name] = value ? parseAttributeValue(value) : name; }); @@ -23,7 +33,7 @@ function parseAttributes(str: string): Record { * @param source text content to extract tag from * @param tag the tag to extract */ -export function extractTag(source: string, tag: 'script' | 'style') { +export function extractTag(source: string, tag: 'script' | 'style'): TagInformation | null { const exp = new RegExp(`()|(<${tag}(\\s[\\S\\s]*?)?>)([\\S\\s]*?)<\\/${tag}>`, 'igs'); let match = exp.exec(source); @@ -39,12 +49,16 @@ export function extractTag(source: string, tag: 'script' | 'style') { const content = match[4]; const start = match.index + match[2].length; const end = start + content.length; + const startPos = positionAt(start, source); + const endPos = positionAt(end, source); return { content, attributes, start, end, + startPos, + endPos, container: { start: match.index, end: match.index + match[0].length }, }; } diff --git a/packages/language-server/src/plugins/typescript/DocumentMapper.ts b/packages/language-server/src/plugins/typescript/DocumentMapper.ts new file mode 100644 index 000000000..f6eb359ed --- /dev/null +++ b/packages/language-server/src/plugins/typescript/DocumentMapper.ts @@ -0,0 +1,93 @@ +import { Position } from 'vscode-languageserver'; +import { SourceMapConsumer } from 'source-map'; +import { TagInformation, offsetAt, positionAt } from '../../lib/documents'; + +export interface DocumentMapper { + getOriginalPosition(generatedPosition: Position): Position; + getGeneratedPosition(originalPosition: Position): Position; +} + +export class IdentityMapper implements DocumentMapper { + getOriginalPosition(generatedPosition: Position): Position { + return generatedPosition; + } + + getGeneratedPosition(originalPosition: Position): Position { + return originalPosition; + } +} + +export class FragmentMapper implements DocumentMapper { + constructor(private originalText: string, private tagInfo: TagInformation) {} + + 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.originalText); + } +} + +export class ConsumerDocumentMapper implements DocumentMapper { + constructor( + private consumer: SourceMapConsumer, + private sourceUri: string, + private nrPrependesLines: number, + ) {} + + getOriginalPosition(generatedPosition: Position): Position { + generatedPosition = Position.create( + generatedPosition.line - this.nrPrependesLines, + generatedPosition.character, + ); + + const mapped = this.consumer.originalPositionFor({ + line: generatedPosition.line + 1, + column: generatedPosition.character, + }); + + if (!mapped) { + return { line: -1, character: -1 }; + } + + if (mapped.line === 0) { + console.warn('Got 0 mapped line from', generatedPosition, 'col was', mapped.column); + } + + return { + line: (mapped.line || 0) - 1, + character: mapped.column || 0, + }; + } + + getGeneratedPosition(originalPosition: Position): Position { + 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; + } + + result.line += this.nrPrependesLines; + return result; + } +} diff --git a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts index 7e9f14fa4..a9f830c20 100644 --- a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts +++ b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts @@ -1,24 +1,224 @@ import ts from 'typescript'; -import { getScriptKindFromAttributes } from './utils'; -import { TypescriptDocument } from './TypescriptDocument'; +import { getScriptKindFromAttributes, isSvelteFilePath, getScriptKindFromFileName } from './utils'; +import { + Fragment, + positionAt, + offsetAt, + Document, + extractTag, + TagInformation, +} from '../../lib/documents'; +import { + DocumentMapper, + IdentityMapper, + ConsumerDocumentMapper, + FragmentMapper, +} from './DocumentMapper'; +import { Position, Range } from 'vscode-languageserver'; +import { SourceMapConsumer, RawSourceMap } from 'source-map'; +import { pathToUrl, isInRange } from '../../utils'; +import svelte2tsx from 'svelte2tsx'; -export interface DocumentSnapshot extends ts.IScriptSnapshot { - version: number; - scriptKind: ts.ScriptKind; +export interface ParserError { + message: string; + range: Range; + code: number; } export const INITIAL_VERSION = 0; -export namespace DocumentSnapshot { - export function fromDocument(document: TypescriptDocument): DocumentSnapshot { - const text = document.getText(); - const length = document.getTextLength(); - return { - version: document.version, - scriptKind: getScriptKindFromAttributes(document.getAttributes()), - getText: (start, end) => text.substring(start, end), - getLength: () => length, - getChangeRange: () => undefined, - }; +export class DocumentSnapshot implements ts.IScriptSnapshot { + private fragment?: SnapshotFragment; + + static fromDocument(document: Document) { + const { + tsxMap, + text, + scriptInfo, + styleInfo, + parserError, + nrPrependedLines, + } = DocumentSnapshot.preprocessIfIsSvelteFile(document.uri, document.getText()); + + return new DocumentSnapshot( + document.version, + getScriptKindFromAttributes(extractTag(document.getText(), 'script')?.attributes ?? {}), + document.getFilePath() || '', + parserError, + scriptInfo, + styleInfo, + text, + document.getText(), + nrPrependedLines, + tsxMap, + ); + } + + static fromFilePath(filePath: string) { + const originalText = ts.sys.readFile(filePath) ?? ''; + const { + text, + tsxMap, + scriptInfo, + styleInfo, + parserError, + nrPrependedLines, + } = DocumentSnapshot.preprocessIfIsSvelteFile(pathToUrl(filePath), originalText); + + return new DocumentSnapshot( + INITIAL_VERSION + 1, // ensure it's greater than initial build + getScriptKindFromFileName(filePath), + filePath, + parserError, + scriptInfo, + styleInfo, + text, + originalText, + nrPrependedLines, + tsxMap, + ); + } + + private static preprocessIfIsSvelteFile(uri: string, text: string) { + let tsxMap: RawSourceMap | undefined; + let parserError: ParserError | null = null; + const scriptInfo = extractTag(text, 'script'); + const styleInfo = extractTag(text, 'style'); + let nrPrependedLines = 0; + + if (isSvelteFilePath(uri)) { + try { + const tsx = svelte2tsx(text); + text = tsx.code; + tsxMap = tsx.map; + if (tsxMap) { + tsxMap.sources = [uri]; + + const tsCheck = scriptInfo?.content.match(tsCheckRegex); + if (tsCheck) { + // second-last entry is the capturing group with the exact ts-check wording + text = `//${tsCheck[tsCheck.length - 3]}${ts.sys.newLine}` + text; + nrPrependedLines = 1; + } + } + } catch (e) { + // Error start/end logic is different and has different offsets for line, so we need to convert that + const start: Position = { + line: e.start?.line - 1 ?? 0, + character: e.start?.column ?? 0, + }; + const end: Position = e.end + ? { line: e.end.line - 1, character: e.end.column } + : start; + parserError = { + range: { start, end }, + message: e.message, + code: -1, + }; + // fall back to extracted script, if any + text = scriptInfo ? scriptInfo.content : ''; + } + } + + return { tsxMap, text, scriptInfo, styleInfo, parserError, nrPrependedLines }; + } + + private constructor( + public version: number, + public readonly scriptKind: ts.ScriptKind, + public readonly filePath: string, + public readonly parserError: ParserError | null, + public readonly scriptInfo: TagInformation | null, + public readonly styleInfo: TagInformation | null, + private readonly text: string, + private readonly originalText: string, + private readonly nrPrependedLines: number, + private readonly tsxMap?: RawSourceMap, + ) {} + + getText(start: number, end: number) { + return this.text.substring(start, end); + } + + getLength() { + return this.text.length; + } + + getChangeRange() { + return undefined; + } + + positionAt(offset: number) { + return positionAt(offset, this.text); + } + + async getFragment() { + if (!this.fragment) { + const uri = pathToUrl(this.filePath); + const mapper = !this.scriptInfo + ? new IdentityMapper() + : !this.tsxMap + ? new FragmentMapper(this.originalText, this.scriptInfo) + : new ConsumerDocumentMapper( + await new SourceMapConsumer(this.tsxMap), + uri, + this.nrPrependedLines, + ); + this.fragment = new SnapshotFragment( + mapper, + this.text, + this.scriptInfo, + this.styleInfo, + uri, + ); + } + return this.fragment; + } +} + +export class SnapshotFragment implements Fragment { + constructor( + private readonly mapper: DocumentMapper, + public readonly text: string, + public readonly scriptInfo: TagInformation | null, + public readonly styleInfo: TagInformation | null, + private readonly url: string, + ) {} + + positionInParent(pos: Position): Position { + return this.mapper.getOriginalPosition(pos); + } + + positionInFragment(pos: Position): Position { + return this.mapper.getGeneratedPosition(pos); + } + + isInFragment(pos: Position): boolean { + return ( + !this.styleInfo || + !isInRange(Range.create(this.styleInfo.startPos, this.styleInfo.endPos), pos) + ); + } + + getURL(): string { + return this.url; + } + + positionAt(offset: number) { + return positionAt(offset, this.text); + } + + offsetAt(position: Position) { + return offsetAt(position, this.text); } } + +// The following regex matches @ts-check or @ts-nocheck if: +// - it is before the first line of code (so other lines with comments before it are ok) +// - must be @ts-(no)check +// - the comment which has @ts-(no)check can have any type of whitespace before it, but not other characters +// - what's coming after @ts-(no)check is irrelevant as long there is any kind of whitespace or line break, so this would be picked up, too: // @ts-check asdasd +// [ \t\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff] +// is just \s (a.k.a any whitespace character) without linebreak and vertical tab +// eslint-disable-next-line +const tsCheckRegex = /^(\s*(\/\/[ \t\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\S]*)*\s*)*(\/\/[ \t\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*(@ts-(no)?check)($|\s))/; diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResovler.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResovler.ts index a4df60c1e..da59590b4 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResovler.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResovler.ts @@ -1,10 +1,13 @@ import { DocumentManager, Document } from '../../lib/documents'; import { pathToUrl } from '../../utils'; import { getLanguageServiceForDocument } from './service'; -import { TypescriptDocument } from './TypescriptDocument'; +import { DocumentSnapshot } from './DocumentSnapshot'; +import { findTsConfigPath } from './utils'; +import { SnapshotManager } from './SnapshotManager'; export class LSAndTSDocResovler { - constructor(private readonly docManager: DocumentManager) { } + constructor(private readonly docManager: DocumentManager) {} + private createDocument = (fileName: string, content: string) => { const uri = pathToUrl(fileName); const document = this.docManager.openDocument({ @@ -14,16 +17,34 @@ export class LSAndTSDocResovler { version: 0, }); this.docManager.lockDocument(uri); - return new TypescriptDocument(document); + return document; }; - private documents = new Map(); - public getLSAndTSDoc(document: Document) { - let tsDoc = this.documents.get(document); + + getLSAndTSDoc(document: Document) { + const lang = getLanguageServiceForDocument(document, this.createDocument); + const filePath = document.getFilePath()!; + const tsDoc = this.getSnapshot(filePath, document); + + return { tsDoc, lang }; + } + + getSnapshot(filePath: string, document?: Document) { + const snapshotManager = this.getSnapshotManager(filePath); + + let tsDoc = snapshotManager.get(filePath); if (!tsDoc) { - tsDoc = new TypescriptDocument(document); - this.documents.set(document, tsDoc); + tsDoc = document + ? DocumentSnapshot.fromDocument(document) + : DocumentSnapshot.fromFilePath(filePath); + snapshotManager.set(filePath, tsDoc); } - const lang = getLanguageServiceForDocument(tsDoc, this.createDocument); - return { tsDoc, lang }; + + return tsDoc; + } + + getSnapshotManager(fileName: string) { + const tsconfigPath = findTsConfigPath(fileName); + const snapshotManager = SnapshotManager.getFromTsConfigPath(tsconfigPath); + return snapshotManager; } } diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts index bab71c369..a3ad64298 100644 --- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts +++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts @@ -27,7 +27,8 @@ export class SnapshotManager { delete(fileName: string) { return this.documents.delete(fileName); } + getFileNames() { return Array.from(this.documents.keys()); } -} \ No newline at end of file +} diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index dd44b66f7..322ef3374 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -2,8 +2,11 @@ import ts, { NavigationTree } from 'typescript'; import { CodeAction, CodeActionContext, + CompletionContext, DefinitionLink, Diagnostic, + DiagnosticSeverity, + FileChangeType, Hover, LocationLink, Position, @@ -12,64 +15,58 @@ import { TextDocumentEdit, TextEdit, VersionedTextDocumentIdentifier, - FileChangeType, - CompletionContext, } from 'vscode-languageserver'; import { - DocumentManager, - TextDocument, Document, + DocumentManager, mapDiagnosticToParent, mapHoverToParent, + mapRangeToParent, mapSymbolInformationToParent, - mapLocationLinkToParent, - mapCodeActionToParent, } from '../../lib/documents'; import { LSConfigManager, LSTypescriptConfig } from '../../ls-config'; import { pathToUrl } from '../../utils'; import { - convertRange, - getScriptKindFromAttributes, - mapSeverity, - symbolKindFromString, - findTsConfigPath, - getScriptKindFromFileName, -} from './utils'; -import { + AppCompletionItem, + AppCompletionList, CodeActionsProvider, + CompletionsProvider, DefinitionsProvider, DiagnosticsProvider, DocumentSymbolsProvider, HoverProvider, OnRegister, - Resolvable, OnWatchFileChanges, - CompletionsProvider, - AppCompletionItem, - AppCompletionList, } from '../interfaces'; -import { SnapshotManager } from './SnapshotManager'; -import { DocumentSnapshot, INITIAL_VERSION } from './DocumentSnapshot'; -import { CompletionEntryWithIdentifer, CompletionsProviderImpl } from './features/CompletionProvider'; +import { DocumentSnapshot, SnapshotFragment } from './DocumentSnapshot'; +import { + CompletionEntryWithIdentifer, + CompletionsProviderImpl, +} from './features/CompletionProvider'; import { LSAndTSDocResovler } from './LSAndTSDocResovler'; +import { + convertRange, + convertToLocationRange, + getScriptKindFromFileName, + mapSeverity, + symbolKindFromString, +} from './utils'; export class TypeScriptPlugin implements - OnRegister, - DiagnosticsProvider, - HoverProvider, - DocumentSymbolsProvider, - DefinitionsProvider, - CodeActionsProvider, - OnWatchFileChanges, - CompletionsProvider { + OnRegister, + DiagnosticsProvider, + HoverProvider, + DocumentSymbolsProvider, + DefinitionsProvider, + CodeActionsProvider, + OnWatchFileChanges, + CompletionsProvider { private configManager!: LSConfigManager; private readonly lsAndTsDocResolver: LSAndTSDocResovler; private readonly completionProvider: CompletionsProviderImpl; - constructor( - docManager: DocumentManager, - ) { + constructor(docManager: DocumentManager) { this.lsAndTsDocResolver = new LSAndTSDocResovler(docManager); this.completionProvider = new CompletionsProviderImpl(this.lsAndTsDocResolver); } @@ -78,74 +75,96 @@ export class TypeScriptPlugin this.configManager = configManager; } - getDiagnostics(document: Document): Diagnostic[] { + async getDiagnostics(document: Document): Promise { if (!this.featureEnabled('diagnostics')) { return []; } const { lang, tsDoc } = this.getLSAndTSDoc(document); - const isTypescript = - getScriptKindFromAttributes(tsDoc.getAttributes()) === ts.ScriptKind.TS; + const isTypescript = tsDoc.scriptKind === ts.ScriptKind.TSX; + + // Document preprocessing failed, show parser error instead + if (tsDoc.parserError) { + return [ + { + range: tsDoc.parserError.range, + severity: DiagnosticSeverity.Error, + source: isTypescript ? 'ts' : 'js', + message: tsDoc.parserError.message, + code: tsDoc.parserError.code, + }, + ]; + } const diagnostics: ts.Diagnostic[] = [ - ...lang.getSyntacticDiagnostics(tsDoc.getFilePath()!), - ...lang.getSuggestionDiagnostics(tsDoc.getFilePath()!), - ...lang.getSemanticDiagnostics(tsDoc.getFilePath()!), + ...lang.getSyntacticDiagnostics(tsDoc.filePath), + ...lang.getSuggestionDiagnostics(tsDoc.filePath), + ...lang.getSemanticDiagnostics(tsDoc.filePath), ]; + const fragment = await tsDoc.getFragment(); + return diagnostics - .map(diagnostic => ({ + .map((diagnostic) => ({ range: convertRange(tsDoc, diagnostic), severity: mapSeverity(diagnostic.category), source: isTypescript ? 'ts' : 'js', message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'), code: diagnostic.code, })) - .map(diagnostic => mapDiagnosticToParent(tsDoc, diagnostic)); + .map((diagnostic) => mapDiagnosticToParent(fragment, diagnostic)) + .filter( + // In some rare cases mapping of diagnostics does not work and produces negative lines. + // We filter out these diagnostics with negative lines because else the LSP (or VSCode?) + // apparently has a hickup and does not show any diagnostics at all. + (diagnostic) => diagnostic.range.start.line >= 0 && diagnostic.range.end.line >= 0, + ); } - doHover(document: Document, position: Position): Hover | null { + async doHover(document: Document, position: Position): Promise { if (!this.featureEnabled('hover')) { return null; } const { lang, tsDoc } = this.getLSAndTSDoc(document); + const fragment = await tsDoc.getFragment(); const info = lang.getQuickInfoAtPosition( - tsDoc.getFilePath()!, - tsDoc.offsetAt(tsDoc.positionInFragment(position)), + tsDoc.filePath, + fragment.offsetAt(fragment.positionInFragment(position)), ); if (!info) { return null; } const contents = ts.displayPartsToString(info.displayParts); - return mapHoverToParent(tsDoc, { - range: convertRange(tsDoc, info.textSpan), + return mapHoverToParent(fragment, { + range: convertRange(fragment, info.textSpan), contents: { language: 'ts', value: contents }, }); } - getDocumentSymbols(document: Document): SymbolInformation[] { + async getDocumentSymbols(document: Document): Promise { if (!this.featureEnabled('documentSymbols')) { return []; } const { lang, tsDoc } = this.getLSAndTSDoc(document); - const navTree = lang.getNavigationTree(tsDoc.getFilePath()!); + const fragment = await tsDoc.getFragment(); + const navTree = lang.getNavigationTree(tsDoc.filePath); const symbols: SymbolInformation[] = []; - collectSymbols(navTree, undefined, symbol => symbols.push(symbol)); + collectSymbols(navTree, undefined, (symbol) => symbols.push(symbol)); const topContainerName = symbols[0].name; return symbols .slice(1) - .map(symbol => { + .map((symbol) => { if (symbol.containerName === topContainerName) { return { ...symbol, containerName: 'script' }; } return symbol; }) - .map(symbol => mapSymbolInformationToParent(tsDoc, symbol)); + .map((symbol) => mapSymbolInformationToParent(fragment, symbol)); function collectSymbols( tree: NavigationTree, @@ -160,10 +179,10 @@ export class TypeScriptPlugin tree.text, symbolKindFromString(tree.kind), Range.create( - tsDoc.positionAt(start.start), - tsDoc.positionAt(end.start + end.length), + fragment.positionAt(start.start), + fragment.positionAt(end.start + end.length), ), - tsDoc.getURL(), + fragment.getURL(), container, ), ); @@ -176,88 +195,79 @@ export class TypeScriptPlugin } } - getCompletions( + async getCompletions( document: Document, position: Position, completionContext?: CompletionContext, - ): AppCompletionList | null { + ): Promise | null> { if (!this.featureEnabled('completions')) { return null; } - return this.completionProvider.getCompletions( - document, - position, - completionContext - ); + return this.completionProvider.getCompletions(document, position, completionContext); } - resolveCompletion( + async resolveCompletion( document: Document, - completionItem: AppCompletionItem): - Resolvable> { - return this.completionProvider.resolveCompletion( - document, - completionItem, - ); + completionItem: AppCompletionItem, + ): Promise> { + return this.completionProvider.resolveCompletion(document, completionItem); } - getDefinitions(document: Document, position: Position): DefinitionLink[] { + async getDefinitions(document: Document, position: Position): Promise { if (!this.featureEnabled('definitions')) { return []; } const { lang, tsDoc } = this.getLSAndTSDoc(document); + const fragment = await tsDoc.getFragment(); const defs = lang.getDefinitionAndBoundSpan( - tsDoc.getFilePath()!, - tsDoc.offsetAt(tsDoc.positionInFragment(position)), + tsDoc.filePath, + fragment.offsetAt(fragment.positionInFragment(position)), ); if (!defs || !defs.definitions) { return []; } - const docs = new Map([[tsDoc.getFilePath()!, tsDoc]]); + const docs = new Map([[tsDoc.filePath, fragment]]); - return defs.definitions - .map(def => { + return await Promise.all( + defs.definitions.map(async (def) => { let defDoc = docs.get(def.fileName); if (!defDoc) { - defDoc = new TextDocument( - pathToUrl(def.fileName), - ts.sys.readFile(def.fileName) || '', - ); + defDoc = await this.getSnapshot(def.fileName).getFragment(); docs.set(def.fileName, defDoc); } return LocationLink.create( pathToUrl(def.fileName), - convertRange(defDoc, def.textSpan), - convertRange(defDoc, def.textSpan), - convertRange(tsDoc, defs.textSpan), + convertToLocationRange(defDoc, def.textSpan), + convertToLocationRange(defDoc, def.textSpan), + convertToLocationRange(fragment, defs.textSpan), ); - }) - .filter(def => !!def) - .map(def => mapLocationLinkToParent(tsDoc, def)) as DefinitionLink[]; + }), + ); } - getCodeActions( + async getCodeActions( document: Document, range: Range, context: CodeActionContext, - ): Resolvable { + ): Promise { if (!this.featureEnabled('codeActions')) { return []; } const { lang, tsDoc } = this.getLSAndTSDoc(document); + const fragment = await tsDoc.getFragment(); - const start = tsDoc.offsetAt(tsDoc.positionInFragment(range.start)); - const end = tsDoc.offsetAt(tsDoc.positionInFragment(range.end)); - const errorCodes: number[] = context.diagnostics.map(diag => Number(diag.code)); + const start = fragment.offsetAt(fragment.positionInFragment(range.start)); + const end = fragment.offsetAt(fragment.positionInFragment(range.end)); + const errorCodes: number[] = context.diagnostics.map((diag) => Number(diag.code)); const codeFixes = lang.getCodeFixesAtPosition( - tsDoc.getFilePath()!, + tsDoc.filePath, start, end, errorCodes, @@ -265,40 +275,39 @@ export class TypeScriptPlugin {}, ); - const docs = new Map([[tsDoc.getFilePath()!, tsDoc]]); - return codeFixes - .map(fix => { + const docs = new Map([[tsDoc.filePath, fragment]]); + return await Promise.all( + codeFixes.map(async (fix) => { + const documentChanges = await Promise.all( + fix.changes.map(async (change) => { + let doc = docs.get(change.fileName); + if (!doc) { + doc = await this.getSnapshot(change.fileName).getFragment(); + docs.set(change.fileName, doc); + } + return TextDocumentEdit.create( + VersionedTextDocumentIdentifier.create( + pathToUrl(change.fileName), + null, + ), + change.textChanges.map((edit) => { + return TextEdit.replace( + mapRangeToParent(doc!, convertRange(doc!, edit.span)), + edit.newText, + ); + }), + ); + }), + ); return CodeAction.create( fix.description, { - documentChanges: fix.changes.map(change => { - let doc = docs.get(change.fileName); - if (!doc) { - doc = new TextDocument( - pathToUrl(change.fileName), - ts.sys.readFile(change.fileName) || '', - ); - docs.set(change.fileName, doc); - } - - return TextDocumentEdit.create( - VersionedTextDocumentIdentifier.create( - pathToUrl(change.fileName), - null, - ), - change.textChanges.map(edit => { - return TextEdit.replace( - convertRange(doc!, edit.span), - edit.newText, - ); - }), - ); - }), + documentChanges, }, fix.fixName, ); - }) - .map(fix => mapCodeActionToParent(tsDoc, fix)); + }), + ); } onWatchFileChanges(fileName: string, changeType: FileChangeType) { @@ -308,25 +317,14 @@ export class TypeScriptPlugin return; } - const tsconfigPath = findTsConfigPath(fileName); - const snapshotManager = SnapshotManager.getFromTsConfigPath(tsconfigPath); + const snapshotManager = this.getSnapshotManager(fileName); if (changeType === FileChangeType.Deleted) { snapshotManager.delete(fileName); return; } - const content = ts.sys.readFile(fileName) ?? ''; - const newSnapshot: DocumentSnapshot = { - getLength: () => content.length, - getText: (start, end) => content.substring(start, end), - getChangeRange: () => undefined, - // ensure it's greater than initial build - version: INITIAL_VERSION + 1, - scriptKind: getScriptKindFromFileName(fileName) - }; - - + const newSnapshot = DocumentSnapshot.fromFilePath(fileName); const previousSnapshot = snapshotManager.get(fileName); if (previousSnapshot) { @@ -340,6 +338,14 @@ export class TypeScriptPlugin return this.lsAndTsDocResolver.getLSAndTSDoc(document); } + private getSnapshot(filePath: string, document?: Document) { + return this.lsAndTsDocResolver.getSnapshot(filePath, document); + } + + private getSnapshotManager(fileName: string) { + return this.lsAndTsDocResolver.getSnapshotManager(fileName); + } + private featureEnabled(feature: keyof LSTypescriptConfig) { return ( this.configManager.enabled('typescript.enable') && @@ -347,4 +353,3 @@ export class TypeScriptPlugin ); } } - diff --git a/packages/language-server/src/plugins/typescript/TypescriptDocument.ts b/packages/language-server/src/plugins/typescript/TypescriptDocument.ts deleted file mode 100644 index b6b9f1ec9..000000000 --- a/packages/language-server/src/plugins/typescript/TypescriptDocument.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Position } from 'vscode-languageserver'; -import { extractTag, Document, Fragment } from '../../lib/documents'; - -export class TypescriptFragment { - private version!: number; - private info!: { - attributes: {}; - start: number; - end: number; - }; - - constructor(private document: Document) { - this.update(); - } - - /** - * Start of fragment within document. - */ - get start(): number { - this.update(); - return this.info.start; - } - - /** - * End of fragment within document. - */ - get end(): number { - this.update(); - return this.info.end; - } - - /** - * Attributes of fragment within document. - */ - get attributes(): Record { - this.update(); - return { ...this.info.attributes, tag: 'script' }; - } - - /** - * Find the tag in the document if we detected a change - */ - private update() { - if (this.document.version === this.version) { - return; - } - - this.version = this.document.version; - const info = extractTag(this.document.getText(), 'script'); - if (info) { - this.info = info; - return; - } - - const length = this.document.getTextLength(); - this.info = { - attributes: {}, - start: length, - end: length, - }; - } -} - -export class TypescriptDocument extends Document implements Fragment { - private typescriptFragment: TypescriptFragment; - public languageId = 'typescript'; - - constructor(private parent: Document) { - super(); - this.typescriptFragment = new TypescriptFragment(parent); - } - - /** - * Get the fragment position relative to the parent - * @param pos Position in fragment - */ - positionInParent(pos: Position): Position { - const parentOffset = this.typescriptFragment.start + this.offsetAt(pos); - return this.parent.positionAt(parentOffset); - } - - /** - * Get the position relative to the start of the fragment - * @param pos Position in parent - */ - positionInFragment(pos: Position): Position { - const fragmentOffset = this.parent.offsetAt(pos) - this.typescriptFragment.start; - return this.positionAt(fragmentOffset); - } - - /** - * Returns true if the given parent position is inside of this fragment - * @param pos Position in parent - */ - isInFragment(pos: Position): boolean { - const offset = this.parent.offsetAt(pos); - return offset >= this.typescriptFragment.start && offset <= this.typescriptFragment.end; - } - - /** - * Get the fragment text from the parent - */ - getText(): string { - return this.parent - .getText() - .slice(this.typescriptFragment.start, this.typescriptFragment.end); - } - - /** - * Returns the length of the fragment as calculated from the start and end positon - */ - getTextLength(): number { - return this.typescriptFragment.end - this.typescriptFragment.start; - } - - /** - * Return the parent file path - */ - getFilePath(): string | null { - return this.parent.getFilePath(); - } - - getURL() { - return this.parent.getURL(); - } - - get version(): number { - return this.parent.version; - } - - set version(version: number) { - // ignore - } - - getAttributes() { - return this.typescriptFragment.attributes; - } -} diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index eee2318ac..68c4a4909 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -1,66 +1,65 @@ import ts from 'typescript'; -import { Position, TextDocumentIdentifier, TextEdit, CompletionList, CompletionContext, CompletionTriggerKind } from 'vscode-languageserver'; import { - CompletionsProvider, - AppCompletionList, - AppCompletionItem, - Resolvable -} from '../../interfaces'; + CompletionContext, + CompletionList, + CompletionTriggerKind, + Position, + Range, + TextDocumentIdentifier, + TextEdit, +} from 'vscode-languageserver'; import { Document, mapCompletionItemToParent, mapRangeToParent } from '../../../lib/documents'; -import { LSAndTSDocResovler } from "../LSAndTSDocResovler"; +import { isNotNullOrUndefined, pathToUrl } from '../../../utils'; +import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces'; +import { SnapshotFragment } from '../DocumentSnapshot'; +import { LSAndTSDocResovler } from '../LSAndTSDocResovler'; import { - scriptElementKindToCompletionItemKind, + convertRange, getCommitCharactersForScriptElement, - convertRange + scriptElementKindToCompletionItemKind, } from '../utils'; -import { TypescriptDocument } from '../TypescriptDocument'; -export interface CompletionEntryWithIdentifer extends - ts.CompletionEntry, TextDocumentIdentifier { +export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDocumentIdentifier { position: Position; } -type validTriggerCharacter = '.' | '"'| "'" | '`' | '/' | '@' | '<' | '#' +type validTriggerCharacter = '.' | '"' | "'" | '`' | '/' | '@' | '<' | '#'; -export class CompletionsProviderImpl - implements CompletionsProvider { - constructor( - private readonly lsAndTsDocResovler: LSAndTSDocResovler - ) { } +export class CompletionsProviderImpl implements CompletionsProvider { + constructor(private readonly lsAndTsDocResovler: LSAndTSDocResovler) {} /** * The language service throws an error if the character is not a valid trigger character. * Also, the completions are worse. * Therefore, only use the characters the typescript compiler treats as valid. */ - private readonly validTriggerCharacters = - ['.', '"', "'", '`', '/', '@', '<', '#'] as const; + private readonly validTriggerCharacters = ['.', '"', "'", '`', '/', '@', '<', '#'] as const; - private isValidTriggerCharacter(character: string | undefined): - character is validTriggerCharacter { + private isValidTriggerCharacter( + character: string | undefined, + ): character is validTriggerCharacter { return this.validTriggerCharacters.includes(character as validTriggerCharacter); } - getCompletions( + async getCompletions( document: Document, position: Position, - completionContext?: CompletionContext - ): AppCompletionList | null { + completionContext?: CompletionContext, + ): Promise | null> { const { lang, tsDoc } = this.lsAndTsDocResovler.getLSAndTSDoc(document); - const filePath = tsDoc.getFilePath(); - + const filePath = tsDoc.filePath; if (!filePath) { return null; } + const triggerCharacter = completionContext?.triggerCharacter; const triggerKind = completionContext?.triggerKind; - const validTriggerCharacter = - this.isValidTriggerCharacter(triggerCharacter) ? triggerCharacter : - undefined; - const isCustomTriggerCharacter = - triggerKind === CompletionTriggerKind.TriggerCharacter; + const validTriggerCharacter = this.isValidTriggerCharacter(triggerCharacter) + ? triggerCharacter + : undefined; + const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter; // ignore any custom trigger character specified in server capabilities // and is not allow by ts @@ -68,9 +67,14 @@ export class CompletionsProviderImpl return null; } + const fragment = await tsDoc.getFragment(); + if (!fragment.isInFragment(position)) { + return null; + } + const completions = lang.getCompletionsAtPosition( filePath, - tsDoc.offsetAt(tsDoc.positionInFragment(position)), + fragment.offsetAt(fragment.positionInFragment(position)), { includeCompletionsForModuleExports: true, triggerCharacter: validTriggerCharacter, @@ -82,77 +86,115 @@ export class CompletionsProviderImpl } const completionItems = completions.entries - .map(comp => this.toCompletionItem(comp, tsDoc.uri, position)) - .map(comp => mapCompletionItemToParent(tsDoc, comp)); + .map((comp) => + this.toCompletionItem(fragment, comp, pathToUrl(tsDoc.filePath), position), + ) + .filter(isNotNullOrUndefined) + .map((comp) => mapCompletionItemToParent(fragment, comp)); return CompletionList.create(completionItems); } private toCompletionItem( + fragment: SnapshotFragment, comp: ts.CompletionEntry, uri: string, - position: Position - ): AppCompletionItem { - const { label, insertText } = this.getCompletionLableAndInsert(comp); + position: Position, + ): AppCompletionItem | null { + const result = this.getCompletionLabelAndInsert(fragment, comp); + if (!result) { + return null; + } + + const { label, insertText, isSvelteComp } = result; return { label, insertText, kind: scriptElementKindToCompletionItemKind(comp.kind), - sortText: comp.sortText, commitCharacters: getCommitCharactersForScriptElement(comp.kind), - preselect: comp.isRecommended, + // Make sure svelte component takes precedence + sortText: isSvelteComp ? '-1' : comp.sortText, + preselect: isSvelteComp ? true : comp.isRecommended, // pass essential data for resolving completion data: { ...comp, uri, - position - } + position, + }, }; } - private getCompletionLableAndInsert(comp: ts.CompletionEntry) { - const { kind, kindModifiers, name } = comp; + private getCompletionLabelAndInsert(fragment: SnapshotFragment, comp: ts.CompletionEntry) { + let { kind, kindModifiers, name, source } = comp; const isScriptElement = kind === ts.ScriptElementKind.scriptElement; const hasModifier = Boolean(comp.kindModifiers); + const isSvelteComp = this.isSvelteComponentImport(`import ${name} from ${source}`); + if (isSvelteComp) { + name = this.changeSvelteComponentName(name); + + if (this.isExistingSvelteComponentImport(fragment, name, source)) { + return null; + } + } + if (isScriptElement && hasModifier) { return { insertText: name, - label: name + kindModifiers + label: name + kindModifiers, + isSvelteComp, + }; + } + if (isSvelteComp && kind === ts.ScriptElementKind.classElement) { + return { + insertText: name, + label: name, + isSvelteComp, }; } return { - label: name + label: name, + isSvelteComp, }; } - resolveCompletion( + private isExistingSvelteComponentImport( + fragment: SnapshotFragment, + name: string, + source?: string, + ): boolean { + const importStatement = new RegExp(`import ${name} from ["'\`][\\s\\S]+\\.svelte["'\`]`); + return !!source && !!fragment.text.match(importStatement); + } + + async resolveCompletion( document: Document, - completionItem: AppCompletionItem): - Resolvable> { + completionItem: AppCompletionItem, + ): Promise> { const { data: comp } = completionItem; const { tsDoc, lang } = this.lsAndTsDocResovler.getLSAndTSDoc(document); - const filePath = tsDoc.getFilePath(); + const filePath = tsDoc.filePath; if (!comp || !filePath) { return completionItem; } + const fragment = await tsDoc.getFragment(); const detail = lang.getCompletionEntryDetails( filePath, - tsDoc.offsetAt(tsDoc.positionInFragment(comp.position)), + fragment.offsetAt(fragment.positionInFragment(comp.position)), comp.name, {}, comp.source, - {} + {}, ); if (detail) { const { detail: itemDetail, - documentation: itemDocumentation + documentation: itemDocumentation, } = this.getCompletionDocument(detail); completionItem.detail = itemDetail; @@ -165,7 +207,7 @@ export class CompletionsProviderImpl for (const action of actions) { for (const change of action.changes) { - edit.push(...this.codeActionChangesToTextEdit(tsDoc, change)); + edit.push(...this.codeActionChangesToTextEdit(document, fragment, change)); } } @@ -175,7 +217,6 @@ export class CompletionsProviderImpl return completionItem; } - private getCompletionDocument(compDetail: ts.CompletionEntryDetails) { const { source, documentation: tsDocumentation, displayParts } = compDetail; let detail: string = ts.displayPartsToString(displayParts); @@ -185,39 +226,83 @@ export class CompletionsProviderImpl detail = `Auto import from ${importPath}\n${detail}`; } - const documentation = tsDocumentation ? - ts.displayPartsToString(tsDocumentation) : - undefined; + const documentation = tsDocumentation + ? ts.displayPartsToString(tsDocumentation) + : undefined; return { documentation, - detail + detail, }; } private codeActionChangesToTextEdit( - tsDoc: TypescriptDocument, - changes: ts.FileTextChanges + doc: Document, + fragment: SnapshotFragment, + changes: ts.FileTextChanges, ): TextEdit[] { - return changes.textChanges.map(change => - this.codeActionChangeToTextEdit(tsDoc, change) + return changes.textChanges.map((change) => + this.codeActionChangeToTextEdit(doc, fragment, change), ); } private codeActionChangeToTextEdit( - tsDoc: TypescriptDocument, - change: ts.TextChange + doc: Document, + fragment: SnapshotFragment, + change: ts.TextChange, ): TextEdit { + if (this.isSvelteComponentImport(change.newText)) { + change.newText = this.changeSvelteComponentImportName(change.newText); + } + + const scriptTagInfo = fragment.scriptInfo; + if (!scriptTagInfo) { + // no script tag defined yet, add it. + return TextEdit.replace( + beginOfDocumentRange, + `${ts.sys.newLine}`, + ); + } + const { span } = change; - // stop newText be placed like this: ${newLine}`, + ); + + assert.deepEqual( + additionalTextEdits![0]?.range, + Range.create(Position.create(0, 0), Position.create(0, 0)), + ); + }) + // this might take longer + .timeout(4000); + + it('resolve auto completion without auto import (a svelte component which was already imported)', async () => { + const { completionProvider, document, docManager } = setup('importcompletions6.svelte'); + // make sure that the ts language service does know about the imported-file file + await openFileToBeImported(docManager, completionProvider); + + const completions = await completionProvider.getCompletions( + document, + Position.create(3, 7), + ); + document.version++; + + const item = completions?.items.find((item) => item.label === 'ImportedFile'); + + assert.equal(item?.additionalTextEdits, undefined); + assert.equal(item?.detail, undefined); + + const { additionalTextEdits } = await completionProvider.resolveCompletion(document, item!); + + assert.strictEqual(additionalTextEdits, undefined); + }) + // this might take longer + .timeout(4000); }); + +function harmonizeNewLines(input?: string) { + return input?.replace(/\r\n/g, '~:~').replace(/\n/g, '~:~').replace(/~:~/g, ts.sys.newLine); +} diff --git a/packages/language-server/test/plugins/typescript/module-loader.test.ts b/packages/language-server/test/plugins/typescript/module-loader.test.ts index b74251d75..ffb7efe57 100644 --- a/packages/language-server/test/plugins/typescript/module-loader.test.ts +++ b/packages/language-server/test/plugins/typescript/module-loader.test.ts @@ -4,6 +4,7 @@ import ts from 'typescript'; import * as svS from '../../../src/plugins/typescript/svelte-sys'; import { DocumentSnapshot } from '../../../src/plugins/typescript/DocumentSnapshot'; import { createSvelteModuleLoader } from '../../../src/plugins/typescript/module-loader'; +import { TextDocument } from '../../../src/lib/documents'; describe('createSvelteModuleLoader', () => { afterEach(() => { @@ -12,13 +13,9 @@ describe('createSvelteModuleLoader', () => { function setup(resolvedModule: ts.ResolvedModuleFull) { const svelteFile = 'const a = "svelte file";'; - const snapshot: DocumentSnapshot = { - getText: (_, __) => svelteFile, - getLength: () => svelteFile.length, - getChangeRange: () => undefined, - scriptKind: ts.ScriptKind.TS, - version: 0, - }; + const snapshot: DocumentSnapshot = DocumentSnapshot.fromDocument( + new TextDocument('', svelteFile), + ); const getSvelteSnapshotStub = sinon.stub().returns(snapshot); const resolveStub = sinon.stub().returns({ @@ -80,7 +77,7 @@ describe('createSvelteModuleLoader', () => { assert.deepStrictEqual(result, [ { - extension: ts.Extension.Ts, + extension: ts.Extension.Jsx, resolvedFileName: 'filename.svelte', }, ]); diff --git a/packages/language-server/test/plugins/typescript/svelte-sys.test.ts b/packages/language-server/test/plugins/typescript/svelte-sys.test.ts index d73bc5529..12be85d39 100644 --- a/packages/language-server/test/plugins/typescript/svelte-sys.test.ts +++ b/packages/language-server/test/plugins/typescript/svelte-sys.test.ts @@ -3,6 +3,7 @@ import sinon from 'sinon'; import ts from 'typescript'; import { DocumentSnapshot } from '../../../src/plugins/typescript/DocumentSnapshot'; import { createSvelteSys } from '../../../src/plugins/typescript/svelte-sys'; +import { TextDocument } from '../../../src/lib/documents'; describe('Svelte Sys', () => { afterEach(() => { @@ -12,13 +13,9 @@ describe('Svelte Sys', () => { function setupLoader() { const tsFile = 'const a = "ts file";'; const svelteFile = 'const a = "svelte file";'; - const snapshot: DocumentSnapshot = { - getText: (_, __) => svelteFile, - getLength: () => svelteFile.length, - getChangeRange: () => undefined, - scriptKind: ts.ScriptKind.TS, - version: 0, - }; + const snapshot: DocumentSnapshot = DocumentSnapshot.fromDocument( + new TextDocument('', svelteFile), + ); const fileExistsStub = sinon.stub().returns(true); const readFileStub = sinon.stub().returns(tsFile); const getSvelteSnapshotStub = sinon.stub().returns(snapshot); diff --git a/packages/language-server/test/plugins/typescript/testfiles/codeactions.svelte b/packages/language-server/test/plugins/typescript/testfiles/codeactions.svelte index 1e82c56c0..11b83a4ed 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/codeactions.svelte +++ b/packages/language-server/test/plugins/typescript/testfiles/codeactions.svelte @@ -1 +1,3 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/completionsstyle.svelte b/packages/language-server/test/plugins/typescript/testfiles/completionsstyle.svelte new file mode 100644 index 000000000..ee9a4dac4 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/completionsstyle.svelte @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/definitions.svelte b/packages/language-server/test/plugins/typescript/testfiles/definitions.svelte index 65fe48359..fc11c5451 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/definitions.svelte +++ b/packages/language-server/test/plugins/typescript/testfiles/definitions.svelte @@ -1 +1,8 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics-js-notypecheck.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics-js-notypecheck.svelte new file mode 100644 index 000000000..c561556a8 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics-js-notypecheck.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics-js-typecheck.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics-js-typecheck.svelte new file mode 100644 index 000000000..671a78eba --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics-js-typecheck.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics-parsererror.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics-parsererror.svelte new file mode 100644 index 000000000..4d87b0f69 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics-parsererror.svelte @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics.svelte index dce84adfe..8a1291428 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/diagnostics.svelte +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics.svelte @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/documentation.svelte b/packages/language-server/test/plugins/typescript/testfiles/documentation.svelte index e546f4c6b..175671ca0 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/documentation.svelte +++ b/packages/language-server/test/plugins/typescript/testfiles/documentation.svelte @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/importcompletions1.svelte b/packages/language-server/test/plugins/typescript/testfiles/importcompletions1.svelte new file mode 100644 index 000000000..541cb5f16 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/importcompletions1.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/importcompletions2.svelte b/packages/language-server/test/plugins/typescript/testfiles/importcompletions2.svelte new file mode 100644 index 000000000..b04f6cd6f --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/importcompletions2.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/importcompletions3.svelte b/packages/language-server/test/plugins/typescript/testfiles/importcompletions3.svelte new file mode 100644 index 000000000..3c6d6c947 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/importcompletions3.svelte @@ -0,0 +1 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/importcompletions4.svelte b/packages/language-server/test/plugins/typescript/testfiles/importcompletions4.svelte new file mode 100644 index 000000000..e8de45772 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/importcompletions4.svelte @@ -0,0 +1,3 @@ + + +import ImportedFile from './imported-file.svelte'; + +export let prop: string; \ No newline at end of file diff --git a/packages/language-server/test/utils.test.ts b/packages/language-server/test/utils.test.ts new file mode 100644 index 000000000..da6cde414 --- /dev/null +++ b/packages/language-server/test/utils.test.ts @@ -0,0 +1,32 @@ +import { isBeforeOrEqualToPosition } from '../src/utils'; +import { Position } from 'vscode-languageserver'; +import * as assert from 'assert'; + +describe('utils', () => { + describe('#isBeforeOrEqualToPosition', () => { + it('is before position (line, character lower)', () => { + const result = isBeforeOrEqualToPosition(Position.create(1, 1), Position.create(0, 0)); + assert.equal(result, true); + }); + + it('is before position (line lower, character higher)', () => { + const result = isBeforeOrEqualToPosition(Position.create(1, 1), Position.create(0, 2)); + assert.equal(result, true); + }); + + it('is equal to position', () => { + const result = isBeforeOrEqualToPosition(Position.create(1, 1), Position.create(1, 1)); + assert.equal(result, true); + }); + + it('is after position (line, character higher)', () => { + const result = isBeforeOrEqualToPosition(Position.create(1, 1), Position.create(2, 2)); + assert.equal(result, false); + }); + + it('is after position (line lower, character higher)', () => { + const result = isBeforeOrEqualToPosition(Position.create(1, 1), Position.create(2, 0)); + assert.equal(result, false); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 8eb34e03c..cf33a7739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1616,6 +1616,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +svelte2tsx@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/svelte2tsx/-/svelte2tsx-0.1.4.tgz#e3d169fbcb434e7cf7a974e8a01fb5655bf1de6e" + integrity sha512-7JQSBPVHd6Grx6GvKTed7do96MCumEeL6pmked9Y/UKLQKalVRnkdjtydEmoWA7Rld0xPMHDV9L1xYEKl7GgUg== + svelte@3.19.2: version "3.19.2" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.19.2.tgz#4b0169ee33b37399f08eb92163593a0a46c242c7"