Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(fix) store rework #1531

Merged
merged 19 commits into from Jun 21, 2022
45 changes: 33 additions & 12 deletions packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts
Expand Up @@ -68,7 +68,11 @@ import { SemanticTokensProviderImpl } from './features/SemanticTokensProvider';
import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider';
import { TypeDefinitionProviderImpl } from './features/TypeDefinitionProvider';
import { UpdateImportsProviderImpl } from './features/UpdateImportsProvider';
import { isNoTextSpanInGeneratedCode, SnapshotMap } from './features/utils';
import {
is$storeVariableIn$storeDeclaration,
isTextSpanInGeneratedCode,
SnapshotMap
} from './features/utils';
import { LSAndTSDocResolver } from './LSAndTSDocResolver';
import { ignoredBuildDirectories } from './SnapshotManager';
import { isAttributeName, isAttributeShorthand, isEventHandler } from './svelte-ast-utils';
Expand Down Expand Up @@ -344,19 +348,36 @@ export class TypeScriptPlugin

const result = await Promise.all(
defs.definitions.map(async (def) => {
const snapshot = await snapshots.retrieve(def.fileName);
if (def.fileName.endsWith('svelte-shims.d.ts')) {
return;
}

if (
!def.fileName.endsWith('svelte-shims.d.ts') &&
isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)
) {
return LocationLink.create(
pathToUrl(def.fileName),
convertToLocationRange(snapshot, def.textSpan),
convertToLocationRange(snapshot, def.textSpan),
convertToLocationRange(tsDoc, defs.textSpan)
);
let snapshot = await snapshots.retrieve(def.fileName);

// Go from generated $store to store if user wants to find definition for $store
if (isTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) {
if (
!is$storeVariableIn$storeDeclaration(
snapshot.getFullText(),
def.textSpan.start
)
) {
return;
}
// there will be exactly one definition, the store
def = lang.getDefinitionAndBoundSpan(
tsDoc.filePath,
tsDoc.getFullText().indexOf(');', def.textSpan.start) - 1
)!.definitions![0];
snapshot = await snapshots.retrieve(def.fileName);
}

return LocationLink.create(
pathToUrl(def.fileName),
convertToLocationRange(snapshot, def.textSpan),
convertToLocationRange(snapshot, def.textSpan),
convertToLocationRange(tsDoc, defs.textSpan)
);
})
);
return result.filter(isNotNullOrUndefined);
Expand Down
Expand Up @@ -25,7 +25,7 @@ import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { changeSvelteComponentName, convertRange } from '../utils';
import { CompletionsProviderImpl } from './CompletionProvider';
import { findContainingNode, isNoTextSpanInGeneratedCode, SnapshotMap } from './utils';
import { findContainingNode, isTextSpanInGeneratedCode, SnapshotMap } from './utils';

/**
* TODO change this to protocol constant if it's part of the protocol
Expand Down Expand Up @@ -289,7 +289,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
);
}

if (!isNoTextSpanInGeneratedCode(snapshot.getFullText(), edit.span)) {
if (isTextSpanInGeneratedCode(snapshot.getFullText(), edit.span)) {
return undefined;
}

Expand Down
Expand Up @@ -17,7 +17,9 @@ import {
findNodeAtSpan,
isReactiveStatement,
isInReactiveStatement,
gatherIdentifiers
gatherIdentifiers,
isStoreVariableIn$storeDeclaration,
get$storeOffsetOf$storeDeclaration
} from './utils';
import { not, flatten, passMap, regexIndexOf, swapRangeStartEndIfNecessary } from '../../../utils';
import { LSConfigManager } from '../../../ls-config';
Expand Down Expand Up @@ -80,9 +82,36 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider {
...lang.getSuggestionDiagnostics(tsDoc.filePath),
...lang.getSemanticDiagnostics(tsDoc.filePath)
];
diagnostics = diagnostics
.filter(isNotGenerated(tsDoc.getText(0, tsDoc.getLength())))
.filter(not(isUnusedReactiveStatementLabel));

const additionalStoreDiagnostics: ts.Diagnostic[] = [];
const notGenerated = isNotGenerated(tsDoc.getFullText());
for (const diagnostic of diagnostics) {
if (!notGenerated(diagnostic)) {
if (isStoreVariableIn$storeDeclaration(tsDoc.getFullText(), diagnostic.start!)) {
const storeName = tsDoc
.getFullText()
.substring(diagnostic.start!, diagnostic.start! + diagnostic.length!);
const storeUsages = lang.findReferences(
tsDoc.filePath,
get$storeOffsetOf$storeDeclaration(tsDoc.getFullText(), diagnostic.start!)
)![0].references;
for (const storeUsage of storeUsages) {
additionalStoreDiagnostics.push({
...diagnostic,
messageText: `Cannot use '${storeName}' as a store. '${storeName}' needs to be an object with a subscribe method on it.\n\n${ts.flattenDiagnosticMessageText(
diagnostic.messageText,
'\n'
)}`,
start: storeUsage.textSpan.start,
length: storeUsage.textSpan.length
});
}
}
}
}
diagnostics.push(...additionalStoreDiagnostics);

diagnostics = diagnostics.filter(notGenerated).filter(not(isUnusedReactiveStatementLabel));
diagnostics = resolveNoopsInReactiveStatements(lang, diagnostics);

return diagnostics
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { FindComponentReferencesProvider } from '../../interfaces';
import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertToLocationRange, hasNonZeroRange } from '../utils';
import { isNoTextSpanInGeneratedCode, SnapshotMap } from './utils';
import { isTextSpanInGeneratedCode, SnapshotMap } from './utils';

const COMPONENT_SUFFIX = '__SvelteComponent_';

Expand Down Expand Up @@ -40,7 +40,7 @@ export class FindComponentReferencesProviderImpl implements FindComponentReferen

const snapshot = await snapshots.retrieve(ref.fileName);

if (!isNoTextSpanInGeneratedCode(snapshot.getFullText(), ref.textSpan)) {
if (isTextSpanInGeneratedCode(snapshot.getFullText(), ref.textSpan)) {
return null;
}

Expand Down
@@ -1,10 +1,19 @@
import type ts from 'typescript';
import { Location, Position, ReferenceContext } from 'vscode-languageserver';
import { Document } from '../../../lib/documents';
import { flatten, isNotNullOrUndefined, pathToUrl } from '../../../utils';
import { FindReferencesProvider } from '../../interfaces';
import { SvelteDocumentSnapshot } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertToLocationRange, hasNonZeroRange } from '../utils';
import { isNoTextSpanInGeneratedCode, SnapshotMap } from './utils';
import {
get$storeOffsetOf$storeDeclaration,
getStoreOffsetOf$storeDeclaration,
is$storeVariableIn$storeDeclaration,
isStoreVariableIn$storeDeclaration,
isTextSpanInGeneratedCode,
SnapshotMap
} from './utils';

export class FindReferencesProviderImpl implements FindReferencesProvider {
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
Expand All @@ -16,48 +25,129 @@ export class FindReferencesProviderImpl implements FindReferencesProvider {
): Promise<Location[] | null> {
const { lang, tsDoc } = await this.getLSAndTSDoc(document);

const references = lang.findReferences(
const rawReferences = lang.findReferences(
tsDoc.filePath,
tsDoc.offsetAt(tsDoc.getGeneratedPosition(position))
);
if (!references) {
if (!rawReferences) {
return null;
}

const snapshots = new SnapshotMap(this.lsAndTsDocResolver);
snapshots.set(tsDoc.filePath, tsDoc);

const references = flatten(rawReferences.map((ref) => ref.references));
references.push(...(await this.getStoreReferences(references, tsDoc, snapshots, lang)));

const locations = await Promise.all(
flatten(references.map((ref) => ref.references)).map(async (ref) => {
if (!context.includeDeclaration && ref.isDefinition) {
return null;
}

const snapshot = await snapshots.retrieve(ref.fileName);

if (!isNoTextSpanInGeneratedCode(snapshot.getFullText(), ref.textSpan)) {
return null;
}

const location = Location.create(
pathToUrl(ref.fileName),
convertToLocationRange(snapshot, ref.textSpan)
);

// Some references are in generated code but not wrapped with explicit ignore comments.
// These show up as zero-length ranges, so filter them out.
if (!hasNonZeroRange(location)) {
return null;
}

return location;
})
references.map(async (ref) => this.mapReference(ref, context, snapshots))
);

return (
locations
.filter(isNotNullOrUndefined)
// Possible $store references are added afterwards, sort for correct order
.sort(sortLocationByFileAndRange)
);
}

/**
* If references of a $store are searched, also find references for the corresponding store
* and vice versa.
*/
private async getStoreReferences(
references: ts.ReferencedSymbolEntry[],
tsDoc: SvelteDocumentSnapshot,
snapshots: SnapshotMap,
lang: ts.LanguageService
): Promise<ts.ReferencedSymbolEntry[]> {
// If user started finding references at $store, find references for store, too
let storeReferences: ts.ReferencedSymbolEntry[] = [];
const storeReference = references.find(
(ref) =>
ref.fileName === tsDoc.filePath &&
isTextSpanInGeneratedCode(tsDoc.getFullText(), ref.textSpan) &&
is$storeVariableIn$storeDeclaration(tsDoc.getFullText(), ref.textSpan.start)
);
if (storeReference) {
const additionalReferences =
lang.findReferences(
tsDoc.filePath,
getStoreOffsetOf$storeDeclaration(
tsDoc.getFullText(),
storeReference.textSpan.start
)
) || [];
storeReferences = flatten(additionalReferences.map((ref) => ref.references));
}

// If user started finding references at store, find references for $store, too
// If user started finding references at $store, find references for $store in other files
const $storeReferences: ts.ReferencedSymbolEntry[] = [];
for (const ref of [...references, ...storeReferences]) {
const snapshot = await snapshots.retrieve(ref.fileName);
if (
!(
isTextSpanInGeneratedCode(snapshot.getFullText(), ref.textSpan) &&
isStoreVariableIn$storeDeclaration(snapshot.getFullText(), ref.textSpan.start)
)
) {
continue;
}
if (storeReference?.fileName === ref.fileName) {
// $store in X -> usages of store -> store in X -> we would add duplicate $store references
continue;
}

const additionalReferences =
lang.findReferences(
snapshot.filePath,
get$storeOffsetOf$storeDeclaration(snapshot.getFullText(), ref.textSpan.start)
) || [];
$storeReferences.push(...flatten(additionalReferences.map((ref) => ref.references)));
}

return [...storeReferences, ...$storeReferences];
}

private async mapReference(
ref: ts.ReferencedSymbolEntry,
context: ReferenceContext,
snapshots: SnapshotMap
) {
if (!context.includeDeclaration && ref.isDefinition) {
return null;
}

const snapshot = await snapshots.retrieve(ref.fileName);

if (isTextSpanInGeneratedCode(snapshot.getFullText(), ref.textSpan)) {
return null;
}

const location = Location.create(
pathToUrl(ref.fileName),
convertToLocationRange(snapshot, ref.textSpan)
);

return locations.filter(isNotNullOrUndefined);
// Some references are in generated code but not wrapped with explicit ignore comments.
// These show up as zero-length ranges, so filter them out.
if (!hasNonZeroRange(location)) {
return null;
}

return location;
}

private async getLSAndTSDoc(document: Document) {
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
}
}

function sortLocationByFileAndRange(l1: Location, l2: Location): number {
const localeCompare = l1.uri.localeCompare(l2.uri);
return localeCompare === 0
? (l1.range.start.line - l2.range.start.line) * 10000 +
(l1.range.start.character - l2.range.start.character)
: localeCompare;
}
Expand Up @@ -20,28 +20,11 @@ export class HoverProviderImpl implements HoverProvider {
}

const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
let info = lang.getQuickInfoAtPosition(tsDoc.filePath, offset);
const info = lang.getQuickInfoAtPosition(tsDoc.filePath, offset);
if (!info) {
return null;
}

const textSpan = info.textSpan;

// show docs of $store instead of store if necessary
const is$store = tsDoc
.getFullText()
.substring(0, info.textSpan.start)
.endsWith('(__sveltets_1_store_get(');
if (is$store) {
const infoFor$store = lang.getQuickInfoAtPosition(
tsDoc.filePath,
textSpan.start + textSpan.length + 3
);
if (infoFor$store) {
info = infoFor$store;
}
}

const declaration = ts.displayPartsToString(info.displayParts);
const documentation = getMarkdownDocumentation(info.documentation, info.tags);

Expand All @@ -51,7 +34,7 @@ export class HoverProviderImpl implements HoverProvider {
.join('\n');

return mapObjWithRangeToOriginal(tsDoc, {
range: convertRange(tsDoc, textSpan),
range: convertRange(tsDoc, info.textSpan),
contents
});
}
Expand Down