Skip to content

Commit 610f70d

Browse files
authored
Merge pull request #54 from zardoy/next
feat: Exclude covered `case`s in switch for strings & enums (enabled with setting by default)
2 parents 4dd7d1a + b38a3fc commit 610f70d

File tree

11 files changed

+202
-7
lines changed

11 files changed

+202
-7
lines changed

README.MD

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,31 @@ Appends *space* to almost all keywords e.g. `const `, like WebStorm does.
113113

114114
Patches `toString()` insert function snippet on number types to remove tabStop.
115115

116-
### Correct Sorting
116+
### Enforce Properties Sorting
117117

118-
(*enabled by default*, but doesn't work properly in new versions for now)
118+
(*disabled by default*) enable with `tsEssentialPlugins.fixSuggestionsSorting`
119+
120+
Try to restore [original](https://github.com/microsoft/TypeScript/issues/49012) properties sorting in some places such as object destructure & dot property access.
121+
122+
### Switch Exclude Covered Cases
123+
124+
(*enabled by default*)
125+
126+
Exclude already covered strings / enums from suggestions ([TS repo issue](https://github.com/microsoft/TypeScript/issues/13711)).
119127

120128
### Mark Code Actions
121129

122130
(*enabled by default* with two settings)
123131

124132
Mark all TS code actions with `🔵`, so you can be sure they're coming from TypeScript, and not some other extension.
125133

134+
### Additional Types Suggestions
135+
136+
```ts
137+
// Adds types in default constraint:
138+
type A<T extends 'foo' | 'bar' = ''> = ...
139+
```
140+
126141
### Builtin CodeFix Fixes
127142
128143
## Even Even More

src/configurationType.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,16 @@ export type Configuration = {
234234
* @default false
235235
*/
236236
patchOutline: boolean
237+
/**
238+
* Exclude covered strings/enum cases in switch
239+
* @default true
240+
*/
241+
switchExcludeCoveredCases: boolean
242+
/**
243+
* Disable useless highlighting,
244+
* @default disable
245+
*/
246+
disableUselessHighlighting: 'disable' | 'inJsxArttributeStrings' | 'inAllStrings'
237247
/**
238248
* Improve JSX attribute completions:
239249
* - enable builtin jsx attribute completion fix

src/emmet.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
import * as vscode from 'vscode'
2-
import { getExtensionSetting, registerExtensionCommand, updateExtensionSetting } from 'vscode-framework'
2+
import { getExtensionSetting, registerExtensionCommand } from 'vscode-framework'
33
import { EmmetResult } from '../typescript/src/ipcTypes'
44
import { sendCommand } from './sendCommand'
55

66
export const registerEmmet = async () => {
77
if (process.env.PLATFORM !== 'web') {
8+
let isEmmetEnabled: boolean
9+
const setIsEmmetEnabled = () => {
10+
isEmmetEnabled = !!vscode.extensions.getExtension('vscode.emmet')
11+
}
12+
13+
setIsEmmetEnabled()
14+
vscode.extensions.onDidChange(setIsEmmetEnabled)
15+
816
const emmet = await import('@vscode/emmet-helper')
917
const reactLangs = ['javascriptreact', 'typescriptreact']
1018
vscode.languages.registerCompletionItemProvider(
1119
reactLangs,
1220
{
1321
async provideCompletionItems(document, position, token, context) {
22+
if (!getExtensionSetting('jsxEmmet')) return
1423
const emmetConfig = vscode.workspace.getConfiguration('emmet')
15-
if (!emmetConfig.excludeLanguages.includes(document.languageId)) return
24+
if (isEmmetEnabled && !emmetConfig.excludeLanguages.includes(document.languageId)) return
1625

1726
const result = await sendCommand<EmmetResult>('emmet-completions', { document, position })
1827
if (!result) return
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export default (entries: ts.CompletionEntry[], program: ts.Program, leftNode: ts.Node) => {
2+
if (!ts.isStringLiteral(leftNode) || !ts.isTypeParameterDeclaration(leftNode.parent.parent) || leftNode.parent.parent.default !== leftNode.parent) return
3+
const typeChecker = program.getTypeChecker()
4+
const { constraint } = leftNode.parent.parent
5+
if (!constraint) return
6+
const type = typeChecker.getTypeAtLocation(constraint)
7+
if (!(type.flags & ts.TypeFlags.Union)) return
8+
const { types } = (type as any) ?? {}
9+
const values: string[] = types.map(({ value }) => (typeof value === 'string' ? value : undefined)).filter(Boolean)
10+
return values.map(
11+
(value): ts.CompletionEntry => ({
12+
name: value,
13+
kind: ts.ScriptElementKind.string,
14+
sortText: '',
15+
}),
16+
)
17+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { findChildContainingPosition } from '../utils'
2+
3+
export default (entries: ts.CompletionEntry[], position: number, node: ts.Node): ts.CompletionEntry[] | undefined => {
4+
// todo-not-sure for now, requires explicit completion trigger
5+
const prevCharIsSpace = node.getSourceFile().getFullText()[position - 1] === ' '
6+
if (!prevCharIsSpace) return
7+
let extendsKeyword = ts.isInterfaceDeclaration(node) && node.end === position - 1
8+
const addOrBoostKeywords = [] as string[]
9+
if (!extendsKeyword) {
10+
const leftNode = findChildContainingPosition(ts, node.getSourceFile(), position - 2)
11+
if (leftNode && ts.isIdentifier(leftNode) && ts.isTypeParameterDeclaration(leftNode.parent)) {
12+
if (!leftNode.parent.constraint) extendsKeyword = true
13+
} else if (leftNode) {
14+
if (ts.isBlock(leftNode)) {
15+
if (ts.isTryStatement(leftNode.parent) && leftNode.parent.tryBlock === leftNode) addOrBoostKeywords.push(...['catch', 'finally'])
16+
else if (ts.isCatchClause(leftNode.parent) && leftNode.parent.block === leftNode) addOrBoostKeywords.push('finally')
17+
}
18+
if (leftNode.kind === ts.SyntaxKind.ExportKeyword) {
19+
addOrBoostKeywords.push(...['const', 'function', 'default', 'from', 'let'])
20+
}
21+
}
22+
}
23+
if (extendsKeyword) addOrBoostKeywords.push('extends')
24+
if (addOrBoostKeywords.length === 0) return
25+
return [
26+
...addOrBoostKeywords.map(keyword => ({ name: keyword, kind: ts.ScriptElementKind.keyword, sortText: '07' })),
27+
...entries.filter(({ name }) => !addOrBoostKeywords.includes(name)),
28+
]
29+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { oneOf } from '@zardoy/utils'
2+
import { cleanupEntryName } from '../utils'
3+
4+
// implementation not even ideal, but it just works for string & enums, which are used in 99% cases
5+
export default (entries: ts.CompletionEntry[], position: number, sourceFile: ts.SourceFile, leftNode: ts.Node) => {
6+
let nodeComp = leftNode
7+
let enumAccessExpr: string | null | undefined
8+
if (ts.isStringLiteral(leftNode)) enumAccessExpr = null
9+
else {
10+
enumAccessExpr = getPropAccessExprRestText(leftNode)
11+
if (!ts.isCaseClause(nodeComp.parent)) nodeComp = leftNode.parent
12+
}
13+
if (enumAccessExpr === undefined) return
14+
let currentClause: ts.CaseClause
15+
// just for type inferrence
16+
const clauses = ts.isCaseClause(nodeComp.parent) && ts.isCaseBlock(nodeComp.parent.parent) ? nodeComp.parent.parent?.clauses : undefined
17+
if (!clauses) return
18+
currentClause = nodeComp.parent as ts.CaseClause
19+
const coveredValues: string[] = []
20+
for (const clause of clauses) {
21+
if (ts.isDefaultClause(clause) || clause === currentClause) continue
22+
const { expression } = clause
23+
if (enumAccessExpr === null) {
24+
if (ts.isStringLiteralLike(expression)) coveredValues.push(expression.text)
25+
} else {
26+
if (getPropAccessExprRestText(expression) === enumAccessExpr) {
27+
coveredValues.push((expression as ts.PropertyAccessExpression).name.text)
28+
}
29+
}
30+
}
31+
return entries.filter(
32+
({ name, kind }) =>
33+
!oneOf(kind, ts.ScriptElementKind.memberVariableElement, ts.ScriptElementKind.enumMemberElement, ts.ScriptElementKind.string) ||
34+
!coveredValues.includes(cleanupEntryName({ name })),
35+
)
36+
}
37+
38+
const getPropAccessExprRestText = (node: ts.Node) => {
39+
let propNode = node
40+
if (ts.isPropertyAccessExpression(node.parent)) {
41+
propNode = node.parent
42+
}
43+
if (!ts.isPropertyAccessExpression(propNode)) return
44+
return propNode.getText().slice(0, propNode.name.getStart() - propNode.getStart() - 1 /* -1 for dot */)
45+
}

typescript/src/completionsAtPosition.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import improveJsxCompletions from './completions/jsxAttributes'
1111
import arrayMethods from './completions/arrayMethods'
1212
import prepareTextForEmmet from './specialCommands/prepareTextForEmmet'
1313
import objectLiteralHelpers from './completions/objectLiteralHelpers'
14+
import switchCaseExcludeCovered from './completions/switchCaseExcludeCovered'
15+
import additionalTypesSuggestions from './completions/additionalTypesSuggestions'
16+
import boostKeywordSuggestions from './completions/boostKeywordSuggestions'
1417

1518
export type PrevCompletionMap = Record<string, { originalName?: string; documentationOverride?: string | ts.SymbolDisplayPart[] }>
1619

@@ -39,6 +42,7 @@ export const getCompletionsAtPosition = (
3942
if (!prior) prior = { entries: [], isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false }
4043
return true
4144
}
45+
const hasSuggestions = prior && prior.entries.filter(({ kind }) => kind !== ts.ScriptElementKind.warning).length !== 0
4246
const node = findChildContainingPosition(ts, sourceFile, position)
4347
/** node that is one character behind
4448
* useful as in most cases we work with node that is behind the cursor */
@@ -86,16 +90,18 @@ export const getCompletionsAtPosition = (
8690
// #endregion
8791
}
8892
}
89-
const addSignatureAccessCompletions = prior?.entries.filter(({ kind }) => kind !== ts.ScriptElementKind.warning).length
90-
? []
91-
: indexSignatureAccessCompletions(position, node, scriptSnapshot, sourceFile, program)
93+
if (leftNode && !hasSuggestions && ensurePrior() && prior) {
94+
prior.entries = additionalTypesSuggestions(prior.entries, program, leftNode) ?? prior.entries
95+
}
96+
const addSignatureAccessCompletions = hasSuggestions ? [] : indexSignatureAccessCompletions(position, node, scriptSnapshot, sourceFile, program)
9297
if (addSignatureAccessCompletions.length && ensurePrior() && prior) {
9398
prior.entries = [...prior.entries, ...addSignatureAccessCompletions]
9499
}
95100

96101
if (!prior) return
97102

98103
if (c('fixSuggestionsSorting')) prior.entries = fixPropertiesSorting(prior.entries, leftNode, sourceFile, program) ?? prior.entries
104+
if (node) prior.entries = boostKeywordSuggestions(prior.entries, position, node) ?? prior.entries
99105

100106
const entryNames = new Set(prior.entries.map(({ name }) => name))
101107
if (c('removeUselessFunctionProps.enable')) prior.entries = prior.entries.filter(e => !['Symbol', 'caller', 'prototype'].includes(e.name))
@@ -176,6 +182,8 @@ export const getCompletionsAtPosition = (
176182
})
177183
}
178184

185+
if (leftNode && c('switchExcludeCoveredCases')) prior.entries = switchCaseExcludeCovered(prior.entries, position, sourceFile, leftNode) ?? prior.entries
186+
179187
prior.entries = arrayMethods(prior.entries, position, sourceFile, c) ?? prior.entries
180188

181189
if (c('improveJsxCompletions') && leftNode) prior.entries = improveJsxCompletions(prior.entries, leftNode, position, sourceFile, c('jsxCompletionsMap'))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { GetConfig } from './types'
2+
import { findChildContainingPosition } from './utils'
3+
4+
export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig) => {
5+
proxy.getDocumentHighlights = (fileName, position, filesToSearch) => {
6+
const prior = languageService.getDocumentHighlights(fileName, position, filesToSearch)
7+
if (!prior) return
8+
if (prior.length !== 1) return prior
9+
const node = findChildContainingPosition(ts, languageService.getProgram()!.getSourceFile(fileName)!, position)
10+
if (!node) return prior
11+
if (c('disableUselessHighlighting') !== 'disable') {
12+
if (ts.isStringLiteralLike(node)) {
13+
if (c('disableUselessHighlighting') === 'inAllStrings') return
14+
else if (ts.isJsxAttribute(node.parent)) return
15+
}
16+
}
17+
return prior
18+
}
19+
}

typescript/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import decorateCodeFixes from './codeFixes'
1717
import decorateReferences from './references'
1818
import handleSpecialCommand from './specialCommands/handle'
1919
import decorateDefinitions from './definitions'
20+
import decorateDocumentHighlights from './documentHighlights'
2021

2122
const thisPluginMarker = Symbol('__essentialPluginsMarker__')
2223

@@ -148,6 +149,7 @@ export = ({ typescript }: { typescript: typeof ts }) => {
148149
decorateSemanticDiagnostics(proxy, info, c)
149150
decorateDefinitions(proxy, info, c)
150151
decorateReferences(proxy, info.languageService, c)
152+
decorateDocumentHighlights(proxy, info.languageService, c)
151153

152154
if (!__WEB__) {
153155
// dedicated syntax server (which is enabled by default), which fires navtree doesn't seem to receive onConfigurationChanged

typescript/src/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export const getLineTextBeforePos = (sourceFile: ts.SourceFile, position: number
5858
return sourceFile.getFullText().slice(position - character, position)
5959
}
6060

61+
export const cleanupEntryName = ({ name }: Pick<ts.CompletionEntry, 'name'>) => {
62+
// intellicode highlighting
63+
return name.replace(/^ /, '')
64+
}
65+
6166
// Workaround esbuild bundle modules
6267
export const nodeModules = __WEB__
6368
? null

0 commit comments

Comments
 (0)