Skip to content

Commit 429c48b

Browse files
authored
feat(huge-feature): add jsx tags to outline! (#37)
1 parent 2988fa0 commit 429c48b

File tree

10 files changed

+296
-8
lines changed

10 files changed

+296
-8
lines changed

README.MD

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# TypeScript Essential Plugins
2+
3+
## Top Features
4+
5+
### JSX Outline
6+
7+
(*disabled by default*) Enable with `tsEssentialPlugins.patchOutline`
8+
9+
Add JSX elements to outline. It also makes sticky scroll works with your tags!
10+
11+
Super recommended for react. Fragments are not rendered.
12+
13+
### Method Snippets
14+
15+
(*enabled by default*)
16+
17+
Expands arrow callbacks with signature snippet with adding additional undo stack!
18+
19+
Example:
20+
21+
```ts
22+
const callback = (arg) => {}
23+
callback -> callback(arg)
24+
```
25+
26+
### Clean Emmet
27+
28+
(*enabled by default*)
29+
30+
You can turn off emmet integration in JSX and stable emmet suggestion will be *always* within JSX elements.
31+
32+
*Why?* <https://github.com/microsoft/vscode/issues/51537>
33+
34+
- supports only tag expansion for now, have 2 modes
35+
36+
### Remove Definition From References
37+
38+
(*enabled by default*)
39+
40+
<https://github.com/microsoft/vscode/issues/160637>
41+
42+
## Minor Useful Features
43+
44+
### Highlight non-function Methods
45+
46+
(*enabled by default*)
47+
48+
Highlights and lifts non-function methods. Also applies for static class methods.
49+
50+
### Remove Useless Code Fixes
51+
52+
(*enabled by default*)
53+
54+
By default removes `Fix Missing Function Declaration` codefix. Possibly to remove more via setting.
55+
56+
### Remove Useless Function Props
57+
58+
(*enabled by default*)
59+
60+
Removes `Symbol`, `caller`, `prototype` completions on function / classes.
61+
62+
### Patch `toString()`
63+
64+
(*enabled by default*)
65+
66+
Patches `toString()` insert function snippet on number types to remove tabStop.
67+
68+
### Keywords Insert Text
69+
70+
(*enabled by default*)
71+
72+
Almost all keywords would insert space after the name of keyword e.g. `extends` -> `extends `
73+
74+
### Correct Sorting
75+
76+
(*enabled by default*, but doesn't work properly in new versions for now)
77+
78+
### Mark Code Actions
79+
80+
(*enabled by default* with two settings)
81+
82+
Mark all TS code actions with `🔵`, so you can be sure they're coming from TypeScript, and not some other extension.
83+
84+
### Builtin CodeFix Fixes

buildTsPlugin.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
//@ts-check
22
import buildTsPlugin from '@zardoy/vscode-utils/build/buildTypescriptPlugin.js'
33

4-
await buildTsPlugin('typescript')
4+
const watch = process.argv[2] === '--watch'
5+
await buildTsPlugin('typescript', undefined, undefined, {
6+
watch,
7+
logLevel: 'info',
8+
sourcemap: watch,
9+
// banner: {
10+
// js: 'const log = (...args) => console.log(...args.map(a => JSON.stringify(a)))',
11+
// },
12+
})

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@
4747
"fs-extra": "^10.1.0",
4848
"type-fest": "^2.13.1",
4949
"typed-jsonfile": "^0.2.1",
50-
"typescript-old": "npm:[email protected]",
5150
"typescript": "^4.8.3",
5251
"typescript-essential-plugins": "workspace:*",
52+
"typescript-old": "npm:[email protected]",
5353
"vitest": "^0.15.1",
5454
"vscode-manifest": "^0.0.4"
5555
},
@@ -64,6 +64,7 @@
6464
"lodash": "^4.17.21",
6565
"lodash.get": "^4.4.2",
6666
"modify-json-file": "^1.2.2",
67+
"require-from-string": "^2.0.2",
6768
"vscode-framework": "^0.0.18"
6869
},
6970
"prettier": {

pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/configurationType.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export type Configuration = {
6060
*/
6161
// 'patchArrayMethods.enable': boolean
6262
/**
63-
* Highlight and lift non-function methods. Also applies for static class methods. Uses `bind`, `call`, `caller` detection.
63+
* Highlight and lift non-function methods. Also applies for static class methods. Uses `bind`, `call`, `caller` detection.
6464
* @default true
6565
* */
6666
'highlightNonFunctionMethods.enable': boolean
@@ -73,7 +73,7 @@ export type Configuration = {
7373
/**
7474
* Mark QuickFixes & refactorings with 🔵
7575
* @default true
76-
* */
76+
*/
7777
'markTsCodeActions.enable': boolean
7878
/**
7979
* Leave empty to disable
@@ -166,4 +166,13 @@ export type Configuration = {
166166
* @default false
167167
*/
168168
supportTsDiagnosticDisableComment: boolean
169+
/**
170+
* Patch TypeScript outline!
171+
* Extend outline with:
172+
* - JSX Elements
173+
* more coming soon...
174+
* Experimental and might not be stable
175+
* @default false
176+
*/
177+
patchOutline: boolean
169178
}

src/extension.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
/* eslint-disable @typescript-eslint/no-require-imports */
12
import * as vscode from 'vscode'
23
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
3-
import {} from 'vscode-framework'
4+
import { extensionCtx, getExtensionSettingId } from 'vscode-framework'
5+
import { pickObj } from '@zardoy/utils'
6+
import { Configuration } from './configurationType'
47

58
export const activate = async () => {
69
const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features')
@@ -17,11 +20,26 @@ export const activate = async () => {
1720
const syncConfig = () => {
1821
console.log('sending configure request for typescript-essential-plugins')
1922
const config = vscode.workspace.getConfiguration().get(process.env.IDS_PREFIX!)
23+
// eslint-disable-next-line curly
24+
if (process.env.PLATFORM === 'node') {
25+
// see comment in plugin
26+
require('fs').writeFileSync(
27+
require('path').join(extensionCtx.extensionPath, './plugin-config.json'),
28+
JSON.stringify(pickObj(config as Configuration, 'patchOutline')),
29+
)
30+
}
31+
2032
api.configurePlugin('typescript-essential-plugins', config)
2133
}
2234

23-
vscode.workspace.onDidChangeConfiguration(({ affectsConfiguration }) => {
24-
if (affectsConfiguration(process.env.IDS_PREFIX!)) syncConfig()
35+
vscode.workspace.onDidChangeConfiguration(async ({ affectsConfiguration }) => {
36+
if (affectsConfiguration(process.env.IDS_PREFIX!)) {
37+
syncConfig()
38+
if (affectsConfiguration(getExtensionSettingId('patchOutline'))) {
39+
await vscode.commands.executeCommand('typescript.restartTsServer')
40+
void vscode.window.showWarningMessage('outline will be updated after text changes')
41+
}
42+
}
2543
})
2644
syncConfig()
2745

typescript/src/dummyLanguageService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export const createLanguageService = (files: Record<string, string>, { useLib =
1616
if (contents === undefined) return
1717
return ts.ScriptSnapshot.fromString(contents)
1818
},
19+
getScriptKind(fileName) {
20+
return ts.ScriptKind.TSX
21+
},
1922
getCurrentDirectory: () => '',
2023
getDefaultLibFileName: options => {
2124
const defaultLibPath = ts.getDefaultLibFilePath(options)

typescript/src/getPatchedNavTree.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type tslib from 'typescript/lib/tsserverlibrary'
2+
import requireFromString from 'require-from-string'
3+
4+
declare const __TS_SEVER_PATH__: string | undefined
5+
6+
const getPatchedNavModule = (ts: typeof tslib) => {
7+
const tsServerPath = typeof __TS_SEVER_PATH__ !== 'undefined' ? __TS_SEVER_PATH__ : require.main!.filename
8+
const mainScript = require('fs').readFileSync(tsServerPath, 'utf8') as string
9+
const startIdx = mainScript.indexOf('var NavigationBar;')
10+
const ph = '(ts.NavigationBar = {}));'
11+
const lines = mainScript.slice(startIdx, mainScript.indexOf(ph) + ph.length).split(/\r?\n/)
12+
const patchPlaces: {
13+
predicateString: string
14+
linesOffset: number
15+
addString?: string
16+
removeLines?: number
17+
}[] = [
18+
{
19+
predicateString: 'function addChildrenRecursively(node)',
20+
linesOffset: 7,
21+
addString: `
22+
case ts.SyntaxKind.JsxSelfClosingElement:
23+
addLeafNode(node)
24+
break;
25+
case ts.SyntaxKind.JsxElement:
26+
startNode(node)
27+
ts.forEachChild(node, addChildrenRecursively);
28+
endNode()
29+
break`,
30+
},
31+
{
32+
predicateString: 'return "<unknown>";',
33+
linesOffset: -2,
34+
addString: `
35+
case ts.SyntaxKind.JsxSelfClosingElement:
36+
return getNameFromJsxTag(node);
37+
case ts.SyntaxKind.JsxElement:
38+
return getNameFromJsxTag(node.openingElement);`,
39+
},
40+
]
41+
for (let { addString, linesOffset, predicateString, removeLines = 0 } of patchPlaces) {
42+
const addTypeIndex = lines.findIndex(line => line.includes(predicateString))
43+
if (addTypeIndex !== -1) {
44+
lines.splice(addTypeIndex + linesOffset, removeLines, ...(addString ? [addString] : []))
45+
}
46+
}
47+
const getModule = requireFromString('module.exports = (ts, getNameFromJsxTag) => {' + lines.join('\n') + 'return NavigationBar;}')
48+
const getNameFromJsxTag = (node: tslib.JsxSelfClosingElement | tslib.JsxOpeningElement) => {
49+
const {
50+
attributes: { properties },
51+
} = node
52+
const tagName = node.tagName.getText()
53+
const addDotAttrs = ['class', 'className']
54+
// TODO refactor to arr
55+
let idAdd = ''
56+
let classNameAdd = ''
57+
properties.forEach(attr => {
58+
if (!ts.isJsxAttribute(attr) || !attr.initializer) return
59+
const attrName = attr.name?.getText()
60+
if (!attrName) return
61+
if (addDotAttrs.includes(attrName)) {
62+
const textAdd = ts.isStringLiteral(attr.initializer) ? attr.initializer.text : ''
63+
for (let char of textAdd.split(' ')) {
64+
if (char) classNameAdd += `.${char}`
65+
}
66+
} else if (attrName === 'id' && ts.isStringLiteral(attr.initializer)) {
67+
idAdd = `#${attr.initializer.text}`
68+
}
69+
})
70+
return tagName + classNameAdd + idAdd
71+
}
72+
return getModule(ts, getNameFromJsxTag)
73+
}
74+
75+
let navModule
76+
77+
export const getNavTreeItems = (ts: typeof tslib, info: tslib.server.PluginCreateInfo, fileName: string) => {
78+
if (!navModule) navModule = getPatchedNavModule(ts)
79+
const program = info.languageService.getProgram()
80+
if (!program) throw new Error('no program')
81+
const sourceFile = program?.getSourceFile(fileName)
82+
if (!sourceFile) throw new Error('no sourceFile')
83+
84+
const cancellationToken = info.languageServiceHost.getCompilerHost?.()?.getCancellationToken?.() ?? {
85+
isCancellationRequested: () => false,
86+
throwIfCancellationRequested: () => {},
87+
}
88+
return navModule.getNavigationTree(sourceFile, cancellationToken)
89+
}

typescript/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { isGoodPositionMethodCompletion } from './isGoodPositionMethodCompletion
1111
import { inspect } from 'util'
1212
import { findChildContainingPosition, getIndentFromPos } from './utils'
1313
import { getParameterListParts } from './snippetForFunctionCall'
14+
import { getNavTreeItems } from './getPatchedNavTree'
15+
import { join } from 'path'
1416

1517
const thisPluginMarker = Symbol('__essentialPluginsMarker__')
1618

@@ -259,6 +261,14 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t
259261
return prior
260262
}
261263

264+
// didecated syntax server (which is enabled by default), which fires navtree doesn't seem to receive onConfigurationChanged
265+
// so we forced to communicate via fs
266+
const config = JSON.parse(ts.sys.readFile(join(__dirname, '../../plugin-config.json'), 'utf8') ?? '{}')
267+
proxy.getNavigationTree = fileName => {
268+
if (c('patchOutline') || config.patchOutline) return getNavTreeItems(ts, info, fileName)
269+
return info.languageService.getNavigationTree(fileName)
270+
}
271+
262272
info.languageService[thisPluginMarker] = true
263273

264274
return proxy

0 commit comments

Comments
 (0)