Skip to content

Commit a593f06

Browse files
committed
feat: now you can quickly disable plugin with new setting: enablePlugin
fix: fix #53
1 parent c03a129 commit a593f06

File tree

5 files changed

+194
-132
lines changed

5 files changed

+194
-132
lines changed

README.MD

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ usersList.map // -> usersList.map((user) => )
6666

6767
## Minor Useful Features
6868

69+
### `enablePlugin` setting
70+
71+
You can quickly disable this plugin functionality by setting this setting to false. Useful for debugging a problem for example.
72+
6973
### Web Support
7074

7175
> Note: when you open TS/JS file in the web for the first time you currently need to switch editors to make everything work!

src/configurationType.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ type ReplaceRule = {
3434
// For easier testing, specify every default
3535
// TODO support scripting
3636
export type Configuration = {
37+
/**
38+
* Controls wether TypeScript Essentials plugin is enabled or not.
39+
* @default true
40+
*/
41+
enablePlugin: boolean
3742
/**
3843
* Removes `Symbol`, `caller`, `prototype` everywhere
3944
* @default true

src/sendCommand.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import * as vscode from 'vscode'
22
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
3+
import { getExtensionSetting } from 'vscode-framework'
34
import { TriggerCharacterCommand } from '../typescript/src/ipcTypes'
45

56
type SendCommandData = {
67
position: vscode.Position
78
document: vscode.TextDocument
89
}
910
export const sendCommand = async <T>(command: TriggerCharacterCommand, sendCommandDataArg?: SendCommandData): Promise<T | undefined> => {
11+
// plugin id disabled, languageService would not understand the special trigger character
12+
if (!getExtensionSetting('enablePlugin')) return
13+
1014
const {
1115
document: { uri },
1216
position,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { oneOf } from '@zardoy/utils'
2+
import { isGoodPositionMethodCompletion } from './completions/isGoodPositionMethodCompletion'
3+
import { getParameterListParts } from './completions/snippetForFunctionCall'
4+
import { GetConfig } from './types'
5+
6+
export default (
7+
languageService: ts.LanguageService,
8+
c: GetConfig,
9+
fileName: string,
10+
position: number,
11+
sourceFile: ts.SourceFile,
12+
prior: ts.CompletionEntryDetails,
13+
) => {
14+
if (
15+
c('enableMethodSnippets') &&
16+
oneOf(
17+
prior.kind,
18+
ts.ScriptElementKind.constElement,
19+
ts.ScriptElementKind.letElement,
20+
ts.ScriptElementKind.alias,
21+
ts.ScriptElementKind.variableElement,
22+
ts.ScriptElementKind.memberVariableElement,
23+
)
24+
) {
25+
// - 1 to look for possibly previous completing item
26+
let goodPosition = isGoodPositionMethodCompletion(ts, fileName, sourceFile, position - 1, languageService, c)
27+
let rawPartsOverride: ts.SymbolDisplayPart[] | undefined
28+
if (goodPosition && prior.kind === ts.ScriptElementKind.alias) {
29+
goodPosition = prior.displayParts[5]?.text === 'method' || (prior.displayParts[4]?.kind === 'keyword' && prior.displayParts[4].text === 'function')
30+
const { parts, gotMethodHit, hasOptionalParameters } = getParameterListParts(prior.displayParts)
31+
if (gotMethodHit) rawPartsOverride = hasOptionalParameters ? [...parts, { kind: '', text: ' ' }] : parts
32+
}
33+
const punctuationIndex = prior.displayParts.findIndex(({ kind, text }) => kind === 'punctuation' && text === ':')
34+
if (goodPosition && punctuationIndex !== 1) {
35+
const isParsableMethod = prior.displayParts
36+
// next is space
37+
.slice(punctuationIndex + 2)
38+
.map(({ text }) => text)
39+
.join('')
40+
.match(/^\((.*)\) => /)
41+
if (rawPartsOverride || isParsableMethod) {
42+
let firstArgMeet = false
43+
const args = (
44+
rawPartsOverride ||
45+
prior.displayParts.filter(({ kind }, index, array) => {
46+
if (kind !== 'parameterName') return false
47+
if (array[index - 1]!.text === '(') {
48+
if (!firstArgMeet) {
49+
// bad parsing, as it doesn't take second and more args
50+
firstArgMeet = true
51+
return true
52+
}
53+
return false
54+
}
55+
return true
56+
})
57+
).map(({ text }) => text)
58+
prior = {
59+
...prior,
60+
documentation: [...(prior.documentation ?? []), { kind: 'text', text: `<!-- insert-func: ${args.join(',')}-->` }],
61+
}
62+
}
63+
}
64+
}
65+
return prior
66+
}

typescript/src/index.ts

Lines changed: 115 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ import _ from 'lodash'
77
import { GetConfig } from './types'
88
import { getCompletionsAtPosition, PrevCompletionMap } from './completionsAtPosition'
99
import { TriggerCharacterCommand } from './ipcTypes'
10-
import { oneOf } from '@zardoy/utils'
11-
import { isGoodPositionMethodCompletion } from './completions/isGoodPositionMethodCompletion'
12-
import { getParameterListParts } from './completions/snippetForFunctionCall'
1310
import { getNavTreeItems } from './getPatchedNavTree'
1411
import decorateCodeActions from './codeActions/decorateProxy'
1512
import decorateSemanticDiagnostics from './semanticDiagnostics'
@@ -18,156 +15,142 @@ import decorateReferences from './references'
1815
import handleSpecialCommand from './specialCommands/handle'
1916
import decorateDefinitions from './definitions'
2017
import decorateDocumentHighlights from './documentHighlights'
18+
import completionEntryDetails from './completionEntryDetails'
2119

2220
const thisPluginMarker = Symbol('__essentialPluginsMarker__')
2321

24-
// just to see wether issue is resolved
2522
let _configuration: Configuration
2623
const c: GetConfig = key => get(_configuration, key)
27-
//@ts-ignore
28-
export = ({ typescript }: { typescript: typeof ts }) => {
24+
25+
const decorateLanguageService = (info: ts.server.PluginCreateInfo, existingProxy?: ts.LanguageService) => {
26+
// Set up decorator object
27+
const proxy: ts.LanguageService = existingProxy ?? Object.create(null)
28+
29+
for (const k of Object.keys(info.languageService)) {
30+
const x = info.languageService[k]!
31+
// @ts-expect-error - JS runtime trickery which is tricky to type tersely
32+
proxy[k] = (...args: Array<Record<string, unknown>>) => x.apply(info.languageService, args)
33+
}
34+
35+
const { languageService } = info
36+
37+
let prevCompletionsMap: PrevCompletionMap
38+
// eslint-disable-next-line complexity
39+
proxy.getCompletionsAtPosition = (fileName, position, options) => {
40+
const updateConfigCommand = 'updateConfig'
41+
if (options?.triggerCharacter?.startsWith(updateConfigCommand)) {
42+
_configuration = JSON.parse(options.triggerCharacter.slice(updateConfigCommand.length))
43+
return { entries: [] }
44+
}
45+
const specialCommandResult = options?.triggerCharacter
46+
? handleSpecialCommand(info, fileName, position, options.triggerCharacter as TriggerCharacterCommand, _configuration)
47+
: undefined
48+
// handled specialCommand request
49+
if (specialCommandResult !== undefined) return specialCommandResult as any
50+
prevCompletionsMap = {}
51+
const scriptSnapshot = info.project.getScriptSnapshot(fileName)
52+
// have no idea in which cases its possible, but we can't work without it
53+
if (!scriptSnapshot) return
54+
const result = getCompletionsAtPosition(fileName, position, options, c, info.languageService, scriptSnapshot, ts)
55+
if (!result) return
56+
prevCompletionsMap = result.prevCompletionsMap
57+
return result.completions
58+
}
59+
60+
proxy.getCompletionEntryDetails = (fileName, position, entryName, formatOptions, source, preferences, data) => {
61+
if (fileName === 'disposeLanguageService') {
62+
process.exit(1)
63+
return
64+
}
65+
const program = languageService.getProgram()
66+
const sourceFile = program?.getSourceFile(fileName)
67+
if (!program || !sourceFile) return
68+
const { documentationOverride } = prevCompletionsMap[entryName] ?? {}
69+
if (documentationOverride) {
70+
return {
71+
name: entryName,
72+
kind: ts.ScriptElementKind.alias,
73+
kindModifiers: '',
74+
displayParts: typeof documentationOverride === 'string' ? [{ kind: 'text', text: documentationOverride }] : documentationOverride,
75+
}
76+
}
77+
const prior = languageService.getCompletionEntryDetails(
78+
fileName,
79+
position,
80+
prevCompletionsMap[entryName]?.originalName || entryName,
81+
formatOptions,
82+
source,
83+
preferences,
84+
data,
85+
)
86+
if (!prior) return
87+
return completionEntryDetails(languageService, c, fileName, position, sourceFile, prior)
88+
}
89+
90+
decorateCodeActions(proxy, info.languageService, c)
91+
decorateCodeFixes(proxy, info.languageService, c)
92+
decorateSemanticDiagnostics(proxy, info, c)
93+
decorateDefinitions(proxy, info, c)
94+
decorateReferences(proxy, info.languageService, c)
95+
decorateDocumentHighlights(proxy, info.languageService, c)
96+
97+
if (!__WEB__) {
98+
// dedicated syntax server (which is enabled by default), which fires navtree doesn't seem to receive onConfigurationChanged
99+
// so we forced to communicate via fs
100+
const config = JSON.parse(ts.sys.readFile(require('path').join(__dirname, '../../plugin-config.json'), 'utf8') ?? '{}')
101+
proxy.getNavigationTree = fileName => {
102+
if (c('patchOutline') || config.patchOutline) return getNavTreeItems(ts, info, fileName)
103+
return info.languageService.getNavigationTree(fileName)
104+
}
105+
}
106+
107+
info.languageService[thisPluginMarker] = true
108+
return proxy
109+
}
110+
111+
const updateConfigListeners: Array<() => void> = []
112+
113+
const plugin: ts.server.PluginModuleFactory = ({ typescript }) => {
29114
ts = typescript
30115
return {
31-
create(info: ts.server.PluginCreateInfo) {
116+
create(info) {
32117
// receive fresh config
33118
_configuration = info.config
34119
console.log('receive config', JSON.stringify(_configuration))
35120
if (info.languageService[thisPluginMarker]) return info.languageService
121+
try {
122+
info.languageService.getCompletionEntryDetails('disposeLanguageService', 0, '', undefined, undefined, undefined, undefined)
123+
} catch {}
36124

37-
// Set up decorator object
38-
const proxy: ts.LanguageService = Object.create(null)
39-
40-
for (const k of Object.keys(info.languageService)) {
41-
const x = info.languageService[k]!
42-
// @ts-expect-error - JS runtime trickery which is tricky to type tersely
43-
proxy[k] = (...args: Array<Record<string, unknown>>) => x.apply(info.languageService, args)
44-
}
45-
46-
let prevCompletionsMap: PrevCompletionMap
47-
// eslint-disable-next-line complexity
48-
proxy.getCompletionsAtPosition = (fileName, position, options) => {
49-
const updateConfigCommand = 'updateConfig'
50-
if (options?.triggerCharacter?.startsWith(updateConfigCommand)) {
51-
_configuration = JSON.parse(options.triggerCharacter.slice(updateConfigCommand.length))
52-
return { entries: [] }
53-
}
54-
const specialCommandResult = options?.triggerCharacter
55-
? handleSpecialCommand(info, fileName, position, options.triggerCharacter as TriggerCharacterCommand, _configuration)
56-
: undefined
57-
// handled specialCommand request
58-
if (specialCommandResult !== undefined) return specialCommandResult as any
59-
prevCompletionsMap = {}
60-
const scriptSnapshot = info.project.getScriptSnapshot(fileName)
61-
// have no idea in which cases its possible, but we can't work without it
62-
if (!scriptSnapshot) return
63-
const result = getCompletionsAtPosition(fileName, position, options, c, info.languageService, scriptSnapshot, ts)
64-
if (!result) return
65-
prevCompletionsMap = result.prevCompletionsMap
66-
return result.completions
67-
}
125+
const proxy = decorateLanguageService(info, undefined)
68126

69-
proxy.getCompletionEntryDetails = (fileName, position, entryName, formatOptions, source, preferences, data) => {
70-
const program = info.languageService.getProgram()
71-
const sourceFile = program?.getSourceFile(fileName)
72-
if (!program || !sourceFile) return
73-
const { documentationOverride } = prevCompletionsMap[entryName] ?? {}
74-
if (documentationOverride) {
75-
return {
76-
name: entryName,
77-
kind: ts.ScriptElementKind.alias,
78-
kindModifiers: '',
79-
displayParts: typeof documentationOverride === 'string' ? [{ kind: 'text', text: documentationOverride }] : documentationOverride,
80-
}
81-
}
82-
let prior = info.languageService.getCompletionEntryDetails(
83-
fileName,
84-
position,
85-
prevCompletionsMap[entryName]?.originalName || entryName,
86-
formatOptions,
87-
source,
88-
preferences,
89-
data,
90-
)
91-
if (!prior) return
92-
if (
93-
c('enableMethodSnippets') &&
94-
oneOf(
95-
prior.kind,
96-
ts.ScriptElementKind.constElement,
97-
ts.ScriptElementKind.letElement,
98-
ts.ScriptElementKind.alias,
99-
ts.ScriptElementKind.variableElement,
100-
ts.ScriptElementKind.memberVariableElement,
101-
)
102-
) {
103-
// - 1 to look for possibly previous completing item
104-
let goodPosition = isGoodPositionMethodCompletion(ts, fileName, sourceFile, position - 1, info.languageService, c)
105-
let rawPartsOverride: ts.SymbolDisplayPart[] | undefined
106-
if (goodPosition && prior.kind === ts.ScriptElementKind.alias) {
107-
goodPosition =
108-
prior.displayParts[5]?.text === 'method' || (prior.displayParts[4]?.kind === 'keyword' && prior.displayParts[4].text === 'function')
109-
const { parts, gotMethodHit, hasOptionalParameters } = getParameterListParts(prior.displayParts)
110-
if (gotMethodHit) rawPartsOverride = hasOptionalParameters ? [...parts, { kind: '', text: ' ' }] : parts
127+
let prevPluginEnabledSetting = _configuration.enablePlugin
128+
updateConfigListeners.push(() => {
129+
if (prevPluginEnabledSetting && !_configuration.enablePlugin) {
130+
// plugin got disabled, restore original languageService methods
131+
for (const key of Object.keys(proxy)) {
132+
//@ts-expect-error
133+
proxy[key] = (...args: Array<Record<string, unknown>>) => info.languageService[key].apply(info.languageService, args)
111134
}
112-
const punctuationIndex = prior.displayParts.findIndex(({ kind, text }) => kind === 'punctuation' && text === ':')
113-
if (goodPosition && punctuationIndex !== 1) {
114-
const isParsableMethod = prior.displayParts
115-
// next is space
116-
.slice(punctuationIndex + 2)
117-
.map(({ text }) => text)
118-
.join('')
119-
.match(/^\((.*)\) => /)
120-
if (rawPartsOverride || isParsableMethod) {
121-
let firstArgMeet = false
122-
const args = (
123-
rawPartsOverride ||
124-
prior.displayParts.filter(({ kind }, index, array) => {
125-
if (kind !== 'parameterName') return false
126-
if (array[index - 1]!.text === '(') {
127-
if (!firstArgMeet) {
128-
// bad parsing, as it doesn't take second and more args
129-
firstArgMeet = true
130-
return true
131-
}
132-
return false
133-
}
134-
return true
135-
})
136-
).map(({ text }) => text)
137-
prior = {
138-
...prior,
139-
documentation: [...(prior.documentation ?? []), { kind: 'text', text: `<!-- insert-func: ${args.join(',')}-->` }],
140-
}
141-
}
142-
}
143-
}
144-
return prior
145-
}
146-
147-
decorateCodeActions(proxy, info.languageService, c)
148-
decorateCodeFixes(proxy, info.languageService, c)
149-
decorateSemanticDiagnostics(proxy, info, c)
150-
decorateDefinitions(proxy, info, c)
151-
decorateReferences(proxy, info.languageService, c)
152-
decorateDocumentHighlights(proxy, info.languageService, c)
153-
154-
if (!__WEB__) {
155-
// dedicated syntax server (which is enabled by default), which fires navtree doesn't seem to receive onConfigurationChanged
156-
// so we forced to communicate via fs
157-
const config = JSON.parse(ts.sys.readFile(require('path').join(__dirname, '../../plugin-config.json'), 'utf8') ?? '{}')
158-
proxy.getNavigationTree = fileName => {
159-
if (c('patchOutline') || config.patchOutline) return getNavTreeItems(ts, info, fileName)
160-
return info.languageService.getNavigationTree(fileName)
135+
} else if (!prevPluginEnabledSetting && _configuration.enablePlugin) {
136+
// plugin got enabled
137+
decorateLanguageService(info, proxy)
161138
}
162-
}
163139

164-
info.languageService[thisPluginMarker] = true
140+
prevPluginEnabledSetting = _configuration.enablePlugin
141+
})
165142

166143
return proxy
167144
},
168-
onConfigurationChanged(config: any) {
145+
onConfigurationChanged(config) {
169146
console.log('update config', JSON.stringify(config))
170147
_configuration = config
148+
for (const updateConfigListener of updateConfigListeners) {
149+
updateConfigListener()
150+
}
171151
},
172152
}
173153
}
154+
155+
//@ts-ignore
156+
export = plugin

0 commit comments

Comments
 (0)