diff --git a/README.MD b/README.MD new file mode 100644 index 00000000..216ed48b --- /dev/null +++ b/README.MD @@ -0,0 +1,84 @@ +# TypeScript Essential Plugins + +## Top Features + +### JSX Outline + +(*disabled by default*) Enable with `tsEssentialPlugins.patchOutline` + +Add JSX elements to outline. It also makes sticky scroll works with your tags! + +Super recommended for react. Fragments are not rendered. + +### Method Snippets + +(*enabled by default*) + +Expands arrow callbacks with signature snippet with adding additional undo stack! + +Example: + +```ts +const callback = (arg) => {} +callback -> callback(arg) +``` + +### Clean Emmet + +(*enabled by default*) + +You can turn off emmet integration in JSX and stable emmet suggestion will be *always* within JSX elements. + +*Why?* + +- supports only tag expansion for now, have 2 modes + +### Remove Definition From References + +(*enabled by default*) + + + +## Minor Useful Features + +### Highlight non-function Methods + +(*enabled by default*) + +Highlights and lifts non-function methods. Also applies for static class methods. + +### Remove Useless Code Fixes + +(*enabled by default*) + +By default removes `Fix Missing Function Declaration` codefix. Possibly to remove more via setting. + +### Remove Useless Function Props + +(*enabled by default*) + +Removes `Symbol`, `caller`, `prototype` completions on function / classes. + +### Patch `toString()` + +(*enabled by default*) + +Patches `toString()` insert function snippet on number types to remove tabStop. + +### Keywords Insert Text + +(*enabled by default*) + +Almost all keywords would insert space after the name of keyword e.g. `extends` -> `extends ` + +### Correct Sorting + +(*enabled by default*, but doesn't work properly in new versions for now) + +### Mark Code Actions + +(*enabled by default* with two settings) + +Mark all TS code actions with `🔵`, so you can be sure they're coming from TypeScript, and not some other extension. + +### Builtin CodeFix Fixes diff --git a/buildTsPlugin.mjs b/buildTsPlugin.mjs index bbf9cbb5..59ecfb32 100644 --- a/buildTsPlugin.mjs +++ b/buildTsPlugin.mjs @@ -1,4 +1,12 @@ //@ts-check import buildTsPlugin from '@zardoy/vscode-utils/build/buildTypescriptPlugin.js' -await buildTsPlugin('typescript') +const watch = process.argv[2] === '--watch' +await buildTsPlugin('typescript', undefined, undefined, { + watch, + logLevel: 'info', + sourcemap: watch, + // banner: { + // js: 'const log = (...args) => console.log(...args.map(a => JSON.stringify(a)))', + // }, +}) diff --git a/package.json b/package.json index 857a8464..59b7b4da 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,9 @@ "fs-extra": "^10.1.0", "type-fest": "^2.13.1", "typed-jsonfile": "^0.2.1", - "typescript-old": "npm:typescript@4.5.4", "typescript": "^4.8.3", "typescript-essential-plugins": "workspace:*", + "typescript-old": "npm:typescript@4.5.4", "vitest": "^0.15.1", "vscode-manifest": "^0.0.4" }, @@ -64,6 +64,7 @@ "lodash": "^4.17.21", "lodash.get": "^4.4.2", "modify-json-file": "^1.2.2", + "require-from-string": "^2.0.2", "vscode-framework": "^0.0.18" }, "prettier": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ecb5e87..8916ddba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,7 @@ importers: lodash: ^4.17.21 lodash.get: ^4.4.2 modify-json-file: ^1.2.2 + require-from-string: ^2.0.2 type-fest: ^2.13.1 typed-jsonfile: ^0.2.1 typescript: ^4.8.3 @@ -39,6 +40,7 @@ importers: lodash: 4.17.21 lodash.get: 4.4.2 modify-json-file: 1.2.2 + require-from-string: 2.0.2 vscode-framework: 0.0.18_w77hramgtzb6kbct6jmnygw2sq devDependencies: '@types/fs-extra': 9.0.13 @@ -3861,6 +3863,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /require-from-string/2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: false + /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} diff --git a/src/configurationType.ts b/src/configurationType.ts index cb96a012..aa454634 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -60,7 +60,7 @@ export type Configuration = { */ // 'patchArrayMethods.enable': boolean /** - * Highlight and lift non-function methods. Also applies for static class methods. Uses `bind`, `call`, `caller` detection. + * Highlight and lift non-function methods. Also applies for static class methods. Uses `bind`, `call`, `caller` detection. * @default true * */ 'highlightNonFunctionMethods.enable': boolean @@ -73,7 +73,7 @@ export type Configuration = { /** * Mark QuickFixes & refactorings with 🔵 * @default true - * */ + */ 'markTsCodeActions.enable': boolean /** * Leave empty to disable @@ -166,4 +166,13 @@ export type Configuration = { * @default false */ supportTsDiagnosticDisableComment: boolean + /** + * Patch TypeScript outline! + * Extend outline with: + * - JSX Elements + * more coming soon... + * Experimental and might not be stable + * @default false + */ + patchOutline: boolean } diff --git a/src/extension.ts b/src/extension.ts index ca5446fe..948ac8d7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,9 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ import * as vscode from 'vscode' import { getActiveRegularEditor } from '@zardoy/vscode-utils' -import {} from 'vscode-framework' +import { extensionCtx, getExtensionSettingId } from 'vscode-framework' +import { pickObj } from '@zardoy/utils' +import { Configuration } from './configurationType' export const activate = async () => { const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features') @@ -17,11 +20,26 @@ export const activate = async () => { const syncConfig = () => { console.log('sending configure request for typescript-essential-plugins') const config = vscode.workspace.getConfiguration().get(process.env.IDS_PREFIX!) + // eslint-disable-next-line curly + if (process.env.PLATFORM === 'node') { + // see comment in plugin + require('fs').writeFileSync( + require('path').join(extensionCtx.extensionPath, './plugin-config.json'), + JSON.stringify(pickObj(config as Configuration, 'patchOutline')), + ) + } + api.configurePlugin('typescript-essential-plugins', config) } - vscode.workspace.onDidChangeConfiguration(({ affectsConfiguration }) => { - if (affectsConfiguration(process.env.IDS_PREFIX!)) syncConfig() + vscode.workspace.onDidChangeConfiguration(async ({ affectsConfiguration }) => { + if (affectsConfiguration(process.env.IDS_PREFIX!)) { + syncConfig() + if (affectsConfiguration(getExtensionSettingId('patchOutline'))) { + await vscode.commands.executeCommand('typescript.restartTsServer') + void vscode.window.showWarningMessage('outline will be updated after text changes') + } + } }) syncConfig() diff --git a/typescript/src/dummyLanguageService.ts b/typescript/src/dummyLanguageService.ts index 9d879759..eb798301 100644 --- a/typescript/src/dummyLanguageService.ts +++ b/typescript/src/dummyLanguageService.ts @@ -16,6 +16,9 @@ export const createLanguageService = (files: Record, { useLib = if (contents === undefined) return return ts.ScriptSnapshot.fromString(contents) }, + getScriptKind(fileName) { + return ts.ScriptKind.TSX + }, getCurrentDirectory: () => '', getDefaultLibFileName: options => { const defaultLibPath = ts.getDefaultLibFilePath(options) diff --git a/typescript/src/getPatchedNavTree.ts b/typescript/src/getPatchedNavTree.ts new file mode 100644 index 00000000..80ee1988 --- /dev/null +++ b/typescript/src/getPatchedNavTree.ts @@ -0,0 +1,89 @@ +import type tslib from 'typescript/lib/tsserverlibrary' +import requireFromString from 'require-from-string' + +declare const __TS_SEVER_PATH__: string | undefined + +const getPatchedNavModule = (ts: typeof tslib) => { + const tsServerPath = typeof __TS_SEVER_PATH__ !== 'undefined' ? __TS_SEVER_PATH__ : require.main!.filename + const mainScript = require('fs').readFileSync(tsServerPath, 'utf8') as string + const startIdx = mainScript.indexOf('var NavigationBar;') + const ph = '(ts.NavigationBar = {}));' + const lines = mainScript.slice(startIdx, mainScript.indexOf(ph) + ph.length).split(/\r?\n/) + const patchPlaces: { + predicateString: string + linesOffset: number + addString?: string + removeLines?: number + }[] = [ + { + predicateString: 'function addChildrenRecursively(node)', + linesOffset: 7, + addString: ` + case ts.SyntaxKind.JsxSelfClosingElement: + addLeafNode(node) + break; + case ts.SyntaxKind.JsxElement: + startNode(node) + ts.forEachChild(node, addChildrenRecursively); + endNode() + break`, + }, + { + predicateString: 'return "";', + linesOffset: -2, + addString: ` + case ts.SyntaxKind.JsxSelfClosingElement: + return getNameFromJsxTag(node); + case ts.SyntaxKind.JsxElement: + return getNameFromJsxTag(node.openingElement);`, + }, + ] + for (let { addString, linesOffset, predicateString, removeLines = 0 } of patchPlaces) { + const addTypeIndex = lines.findIndex(line => line.includes(predicateString)) + if (addTypeIndex !== -1) { + lines.splice(addTypeIndex + linesOffset, removeLines, ...(addString ? [addString] : [])) + } + } + const getModule = requireFromString('module.exports = (ts, getNameFromJsxTag) => {' + lines.join('\n') + 'return NavigationBar;}') + const getNameFromJsxTag = (node: tslib.JsxSelfClosingElement | tslib.JsxOpeningElement) => { + const { + attributes: { properties }, + } = node + const tagName = node.tagName.getText() + const addDotAttrs = ['class', 'className'] + // TODO refactor to arr + let idAdd = '' + let classNameAdd = '' + properties.forEach(attr => { + if (!ts.isJsxAttribute(attr) || !attr.initializer) return + const attrName = attr.name?.getText() + if (!attrName) return + if (addDotAttrs.includes(attrName)) { + const textAdd = ts.isStringLiteral(attr.initializer) ? attr.initializer.text : '' + for (let char of textAdd.split(' ')) { + if (char) classNameAdd += `.${char}` + } + } else if (attrName === 'id' && ts.isStringLiteral(attr.initializer)) { + idAdd = `#${attr.initializer.text}` + } + }) + return tagName + classNameAdd + idAdd + } + return getModule(ts, getNameFromJsxTag) +} + +let navModule + +export const getNavTreeItems = (ts: typeof tslib, info: tslib.server.PluginCreateInfo, fileName: string) => { + if (!navModule) navModule = getPatchedNavModule(ts) + const program = info.languageService.getProgram() + if (!program) throw new Error('no program') + const sourceFile = program?.getSourceFile(fileName) + if (!sourceFile) throw new Error('no sourceFile') + + const cancellationToken = info.languageServiceHost.getCompilerHost?.()?.getCancellationToken?.() ?? { + isCancellationRequested: () => false, + throwIfCancellationRequested: () => {}, + } + return navModule.getNavigationTree(sourceFile, cancellationToken) +} diff --git a/typescript/src/index.ts b/typescript/src/index.ts index 1d1f2ac1..53d4c11f 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -11,6 +11,8 @@ import { isGoodPositionMethodCompletion } from './isGoodPositionMethodCompletion import { inspect } from 'util' import { findChildContainingPosition, getIndentFromPos } from './utils' import { getParameterListParts } from './snippetForFunctionCall' +import { getNavTreeItems } from './getPatchedNavTree' +import { join } from 'path' const thisPluginMarker = Symbol('__essentialPluginsMarker__') @@ -259,6 +261,14 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t return prior } + // didecated syntax server (which is enabled by default), which fires navtree doesn't seem to receive onConfigurationChanged + // so we forced to communicate via fs + const config = JSON.parse(ts.sys.readFile(join(__dirname, '../../plugin-config.json'), 'utf8') ?? '{}') + proxy.getNavigationTree = fileName => { + if (c('patchOutline') || config.patchOutline) return getNavTreeItems(ts, info, fileName) + return info.languageService.getNavigationTree(fileName) + } + info.languageService[thisPluginMarker] = true return proxy diff --git a/typescript/test/completions.spec.ts b/typescript/test/completions.spec.ts index ca362011..9f3ad1c9 100644 --- a/typescript/test/completions.spec.ts +++ b/typescript/test/completions.spec.ts @@ -4,8 +4,12 @@ import type {} from 'vitest/globals' import ts from 'typescript/lib/tsserverlibrary' import { getDefaultConfigFunc } from './defaultSettings' import { isGoodPositionBuiltinMethodCompletion, isGoodPositionMethodCompletion } from '../src/isGoodPositionMethodCompletion' +import { getNavTreeItems } from '../src/getPatchedNavTree' +import { createRequire } from 'module' -const entrypoint = '/test.ts' +const require = createRequire(import.meta.url) + +const entrypoint = '/test.tsx' const files = { [entrypoint]: '' } const { languageService, updateProject } = createLanguageService(files) @@ -116,3 +120,58 @@ test('Function props: cleans & highlights', () => { const entryNamesHighlighted = getCompletionsAtPosition(pos2!)?.entryNames expect(entryNamesHighlighted).includes('☆sync') }) + +test('Patched navtree (outline)', () => { + globalThis.__TS_SEVER_PATH__ = require.resolve('typescript/lib/tsserver') + newFileContents(/* tsx */ ` + const classes = { + header: '...', + title: '...' + } + function A() { + return + before +
+
+ +
+ after + + } + `) + const navTreeItems: ts.NavigationTree = getNavTreeItems(ts, { languageService, languageServiceHost: {} } as any, entrypoint) + const simplify = (items: ts.NavigationTree[]) => { + const newItems: { text: any; childItems? }[] = [] + for (const { text, childItems } of items) { + if (text === 'classes') continue + newItems.push({ text, ...(childItems ? { childItems: simplify(childItems) } : {}) }) + } + return newItems + } + expect(simplify(navTreeItems.childItems ?? [])).toMatchInlineSnapshot(/* json */ ` + [ + { + "childItems": [ + { + "childItems": [ + { + "childItems": [ + { + "text": "div", + }, + { + "text": "span.good", + }, + ], + "text": "div#ok", + }, + ], + "text": "Notification.test.another#yes", + }, + ], + "text": "A", + }, + ] + `) +}) +