Skip to content

Commit

Permalink
Showing 10 changed files with 296 additions and 8 deletions.
84 changes: 84 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
@@ -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?* <https://github.com/microsoft/vscode/issues/51537>

- supports only tag expansion for now, have 2 modes

### Remove Definition From References

(*enabled by default*)

<https://github.com/microsoft/vscode/issues/160637>

## 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
10 changes: 9 additions & 1 deletion buildTsPlugin.mjs
Original file line number Diff line number Diff line change
@@ -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)))',
// },
})
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions src/configurationType.ts
Original file line number Diff line number Diff line change
@@ -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
}
24 changes: 21 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -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()

3 changes: 3 additions & 0 deletions typescript/src/dummyLanguageService.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,9 @@ export const createLanguageService = (files: Record<string, string>, { useLib =
if (contents === undefined) return
return ts.ScriptSnapshot.fromString(contents)
},
getScriptKind(fileName) {
return ts.ScriptKind.TSX
},
getCurrentDirectory: () => '',
getDefaultLibFileName: options => {
const defaultLibPath = ts.getDefaultLibFilePath(options)
89 changes: 89 additions & 0 deletions typescript/src/getPatchedNavTree.ts
Original file line number Diff line number Diff line change
@@ -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 "<unknown>";',
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)
}
10 changes: 10 additions & 0 deletions typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -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
61 changes: 60 additions & 1 deletion typescript/test/completions.spec.ts
Original file line number Diff line number Diff line change
@@ -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 <Notification className="test another" id="yes">
before
<div id="ok">
<div />
<span class="good" />
</div>
after
</Notification>
}
`)
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",
},
]
`)
})

0 comments on commit 429c48b

Please sign in to comment.