Skip to content

Commit

Permalink
(feat) TypeScript plugin
Browse files Browse the repository at this point in the history
Initially support
- rename (doesn't work properly for all kinds of renames yet; need to filter out references inside generated code)
- diagnostics
- find references (need to filter out references inside generated code)

This makes all files TSX hardcoded for now, it seems the TS server is okay with importing tsx into js

sveltejs#580
sveltejs#550
sveltejs#342
sveltejs#110
  • Loading branch information
Simon Holthausen committed May 3, 2021
1 parent c676a77 commit c947c7e
Show file tree
Hide file tree
Showing 14 changed files with 1,188 additions and 8 deletions.
4 changes: 1 addition & 3 deletions packages/typescript-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
src/**/*.js
src/**/*.d.ts
src/tsconfig.tsbuildinfo
dist
node_modules
tsconfig.tsbuildinfo
7 changes: 6 additions & 1 deletion packages/typescript-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
"name": "typescript-svelte-plugin",
"version": "0.1.0",
"description": "A TypeScript Plugin providing Svelte intellisense",
"main": "src/index.js",
"main": "dist/src/index.js",
"scripts": {
"build": "tsc -p ./",
"watch": "tsc -w -p ./",
"test": "echo 'NOOP'"
},
"keywords": [
Expand All @@ -19,5 +20,9 @@
"@tsconfig/node12": "^1.0.0",
"@types/node": "^13.9.0",
"typescript": "*"
},
"dependencies": {
"svelte2tsx": "*",
"sourcemap-codec": "^1.4.8"
}
}
52 changes: 49 additions & 3 deletions packages/typescript-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,56 @@
function init(modules: { typescript: typeof import('typescript/lib/tsserverlibrary') }) {
import { dirname, resolve } from 'path';
import { decorateLanguageService } from './language-service';
import { Logger } from './logger';
import { patchModuleLoader } from './module-loader';
import { SvelteSnapshotManager } from './svelte-snapshots';
import type ts from 'typescript/lib/tsserverlibrary';

function init(modules: { typescript: typeof ts }) {
function create(info: ts.server.PluginCreateInfo) {
// TODO
const logger = new Logger(info.project.projectService.logger);
logger.log('Starting Svelte plugin');

const snapshotManager = new SvelteSnapshotManager(
modules.typescript,
info.project.projectService,
logger,
!!info.project.getCompilerOptions().strict
);

patchCompilerOptions(info.project);
patchModuleLoader(
logger,
snapshotManager,
modules.typescript,
info.languageServiceHost,
info.project
);
return decorateLanguageService(info.languageService, snapshotManager, logger);
}

function getExternalFiles(project: ts.server.ConfiguredProject) {
// TODO
// Needed so the ambient definitions are known inside the tsx files
const svelteTsPath = dirname(require.resolve('svelte2tsx'));
const svelteTsxFiles = [
'./svelte-shims.d.ts',
'./svelte-jsx.d.ts',
'./svelte-native-jsx.d.ts'
].map((f) => modules.typescript.sys.resolvePath(resolve(svelteTsPath, f)));
return svelteTsxFiles;
}

function patchCompilerOptions(project: ts.server.Project) {
const compilerOptions = project.getCompilerOptions();
// Patch needed because svelte2tsx creates jsx/tsx files
compilerOptions.jsx = modules.typescript.JsxEmit.Preserve;

// detect which JSX namespace to use (svelte | svelteNative) if not specified or not compatible
if (!compilerOptions.jsxFactory || !compilerOptions.jsxFactory.startsWith('svelte')) {
// Default to regular svelte, this causes the usage of the "svelte.JSX" namespace
// We don't need to add a switch for svelte-native because the jsx is only relevant
// within Svelte files, which this plugin does not deal with.
compilerOptions.jsxFactory = 'svelte.createElement';
}
}

return { create, getExternalFiles };
Expand Down
46 changes: 46 additions & 0 deletions packages/typescript-plugin/src/language-service/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type ts from 'typescript/lib/tsserverlibrary';
import { Logger } from '../logger';
import { isSvelteFilePath } from '../utils';

export function decorateDiagnostics(ls: ts.LanguageService, logger: Logger) {
decorateSyntacticDiagnostics(ls);
decorateSemanticDiagnostics(ls);
decorateSuggestionDiagnostics(ls);
return ls;
}

function decorateSyntacticDiagnostics(ls: ts.LanguageService): void {
const getSyntacticDiagnostics = ls.getSyntacticDiagnostics;
ls.getSyntacticDiagnostics = (fileName: string) => {
// Diagnostics inside Svelte files are done
// by the svelte-language-server / Svelte for VS Code extension
if (isSvelteFilePath(fileName)) {
return [];
}
return getSyntacticDiagnostics(fileName);
};
}

function decorateSemanticDiagnostics(ls: ts.LanguageService): void {
const getSemanticDiagnostics = ls.getSemanticDiagnostics;
ls.getSemanticDiagnostics = (fileName: string) => {
// Diagnostics inside Svelte files are done
// by the svelte-language-server / Svelte for VS Code extension
if (isSvelteFilePath(fileName)) {
return [];
}
return getSemanticDiagnostics(fileName);
};
}

function decorateSuggestionDiagnostics(ls: ts.LanguageService): void {
const getSuggestionDiagnostics = ls.getSuggestionDiagnostics;
ls.getSuggestionDiagnostics = (fileName: string) => {
// Diagnostics inside Svelte files are done
// by the svelte-language-server / Svelte for VS Code extension
if (isSvelteFilePath(fileName)) {
return [];
}
return getSuggestionDiagnostics(fileName);
};
}
95 changes: 95 additions & 0 deletions packages/typescript-plugin/src/language-service/find-references.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type ts from 'typescript/lib/tsserverlibrary';
import { Logger } from '../logger';
import { SvelteSnapshotManager } from '../svelte-snapshots';
import { isNotNullOrUndefined, isSvelteFilePath } from '../utils';

export function decorateFindReferences(
ls: ts.LanguageService,
snapshotManager: SvelteSnapshotManager,
logger: Logger
): void {
decorateGetReferencesAtPosition(ls, snapshotManager, logger);
_decorateFindReferences(ls, snapshotManager, logger);
}

function _decorateFindReferences(
ls: ts.LanguageService,
snapshotManager: SvelteSnapshotManager,
logger: Logger
) {
const findReferences = ls.findReferences;
ls.findReferences = (fileName, position) => {
const references = findReferences(fileName, position);
return references
?.map((reference) => {
const snapshot = snapshotManager.get(reference.definition.fileName);
if (!isSvelteFilePath(reference.definition.fileName) || !snapshot) {
return reference;
}

const textSpan = snapshot.getOriginalTextSpan(reference.definition.textSpan);
if (!textSpan) {
return null;
}

return {
definition: {
...reference.definition,
textSpan,
// Spare the work for now
originalTextSpan: undefined
},
references: mapReferences(reference.references, snapshotManager, logger)
};
})
.filter(isNotNullOrUndefined);
};
}

function decorateGetReferencesAtPosition(
ls: ts.LanguageService,
snapshotManager: SvelteSnapshotManager,
logger: Logger
) {
const getReferencesAtPosition = ls.getReferencesAtPosition;
ls.getReferencesAtPosition = (fileName, position) => {
const references = getReferencesAtPosition(fileName, position);
return references && mapReferences(references, snapshotManager, logger);
};
}

function mapReferences(
references: ts.ReferenceEntry[],
snapshotManager: SvelteSnapshotManager,
logger: Logger
): ts.ReferenceEntry[] {
return references
.map((reference) => {
const snapshot = snapshotManager.get(reference.fileName);
if (!isSvelteFilePath(reference.fileName) || !snapshot) {
return reference;
}

const textSpan = snapshot.getOriginalTextSpan(reference.textSpan);
if (!textSpan) {
return null;
}

logger.debug(
'Find references; map textSpan: changed',
reference.textSpan,
'to',
textSpan
);

return {
...reference,
textSpan,
// Spare the work for now
contextSpan: undefined,
originalTextSpan: undefined,
originalContextSpan: undefined
};
})
.filter(isNotNullOrUndefined);
}
17 changes: 17 additions & 0 deletions packages/typescript-plugin/src/language-service/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type ts from 'typescript/lib/tsserverlibrary';
import { Logger } from '../logger';
import { SvelteSnapshotManager } from '../svelte-snapshots';
import { decorateDiagnostics } from './diagnostics';
import { decorateFindReferences } from './find-references';
import { decorateRename } from './rename';

export function decorateLanguageService(
ls: ts.LanguageService,
snapshotManager: SvelteSnapshotManager,
logger: Logger
): ts.LanguageService {
decorateRename(ls, snapshotManager, logger);
decorateDiagnostics(ls, logger);
decorateFindReferences(ls, snapshotManager, logger);
return ls;
}
55 changes: 55 additions & 0 deletions packages/typescript-plugin/src/language-service/rename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type ts from 'typescript/lib/tsserverlibrary';
import { Logger } from '../logger';
import { SvelteSnapshotManager } from '../svelte-snapshots';
import { isNotNullOrUndefined, isSvelteFilePath } from '../utils';

export function decorateRename(
ls: ts.LanguageService,
snapshotManager: SvelteSnapshotManager,
logger: Logger
): void {
const findRenameLocations = ls.findRenameLocations;
ls.findRenameLocations = (
fileName,
position,
findInStrings,
findInComments,
providePrefixAndSuffixTextForRename
) => {
const renameLocations = findRenameLocations(
fileName,
position,
findInStrings,
findInComments,
providePrefixAndSuffixTextForRename
);
return renameLocations
?.map((renameLocation) => {
const snapshot = snapshotManager.get(renameLocation.fileName);
if (!isSvelteFilePath(renameLocation.fileName) || !snapshot) {
return renameLocation;
}

// TODO more needed to filter invalid locations, see RenameProvider
const start = snapshot.getOriginalOffset(renameLocation.textSpan.start);
if (start === -1) {
return null;
}

const converted = {
...renameLocation,
textSpan: {
start: start,
length: renameLocation.textSpan.length
}
};
if (converted.contextSpan) {
// Not important, spare the work
converted.contextSpan = undefined;
}
logger.debug('Converted rename location ', converted);
return converted;
})
.filter(isNotNullOrUndefined);
};
}
41 changes: 41 additions & 0 deletions packages/typescript-plugin/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type ts from 'typescript/lib/tsserverlibrary';

export class Logger {
constructor(
private tsLogService: ts.server.Logger,
surpressNonSvelteLogs = false,
private logDebug = false
) {
if (surpressNonSvelteLogs) {
const log = this.tsLogService.info.bind(this.tsLogService);
this.tsLogService.info = (s: string) => {
if (s.startsWith('-Svelte Plugin-')) {
log(s);
}
};
}
}

log(...args: any[]) {
const str = args
.map((arg) => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg);
} catch (e) {
return '[object that cannot by stringified]';
}
}
return arg;
})
.join(' ');
this.tsLogService.info('-Svelte Plugin- ' + str);
}

debug(...args: any[]) {
if (!this.logDebug) {
return;
}
this.log(...args);
}
}
Loading

0 comments on commit c947c7e

Please sign in to comment.