Skip to content

Commit d6a6756

Browse files
committed
fix: enable builtin snippet methods in more places such as alias assigning, object literal expr
fix: expand builtin snippet method when the same completion is accepted fix: enable builtin snippet methods for aliases, such as path.join fix: don't expand our method snippet for object types
1 parent 2a62d64 commit d6a6756

File tree

5 files changed

+129
-23
lines changed

5 files changed

+129
-23
lines changed

src/extension.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const activate = async () => {
3030
const { documentation = '' } = item
3131
const documentationString = documentation instanceof vscode.MarkdownString ? documentation.value : documentation
3232
const insertFuncArgs = /<!-- insert-func: (.*)-->/.exec(documentationString)?.[1]
33+
console.debug('insertFuncArgs', insertFuncArgs)
3334
if (enableMethodSnippets && insertFuncArgs !== undefined) {
3435
const editor = getActiveRegularEditor()!
3536
const startPos = editor.selection.start
@@ -38,8 +39,10 @@ export const activate = async () => {
3839
const snippet = new vscode.SnippetString('')
3940
snippet.appendText('(')
4041
const args = insertFuncArgs.split(',')
41-
for (const [i, arg] of args.entries()) {
42+
for (let [i, arg] of args.entries()) {
4243
if (!arg) continue
44+
// skip empty, but add tabstops if we explicitly want it!
45+
if (arg === ' ') arg = ''
4346
snippet.appendPlaceholder(arg)
4447
if (i !== args.length - 1) snippet.appendText(', ')
4548
}

typescript/src/index.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getCompletionsAtPosition, PrevCompletionMap } from './completionsAtPosi
99
import { oneOf } from '@zardoy/utils'
1010
import { isGoodPositionMethodCompletion } from './isGoodPositionMethodCompletion'
1111
import { inspect } from 'util'
12-
import { getIndentFromPos } from './utils'
12+
import { getParameterListParts } from './snippetForFunctionCall'
1313

1414
const thisPluginMarker = Symbol('__essentialPluginsMarker__')
1515

@@ -77,7 +77,7 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t
7777
displayParts: typeof documentationOverride === 'string' ? [{ kind: 'text', text: documentationOverride }] : documentationOverride,
7878
}
7979
}
80-
const prior = info.languageService.getCompletionEntryDetails(
80+
let prior = info.languageService.getCompletionEntryDetails(
8181
fileName,
8282
position,
8383
prevCompletionsMap[entryName]?.originalName || entryName,
@@ -87,33 +87,48 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t
8787
data,
8888
)
8989
if (!prior) return
90-
if (c('enableMethodSnippets') && oneOf(prior.kind as string, ts.ScriptElementKind.constElement, 'property')) {
91-
const goodPosition = isGoodPositionMethodCompletion(ts, fileName, sourceFile, position, info.languageService)
90+
if (
91+
c('enableMethodSnippets') &&
92+
oneOf(prior.kind as string, ts.ScriptElementKind.constElement, ts.ScriptElementKind.letElement, ts.ScriptElementKind.alias, 'property')
93+
) {
94+
// - 1 to look for possibly previous completing item
95+
let goodPosition = isGoodPositionMethodCompletion(ts, fileName, sourceFile, position - 1, info.languageService)
96+
let rawPartsOverride: ts.SymbolDisplayPart[] | undefined
97+
if (goodPosition && prior.kind === ts.ScriptElementKind.alias) {
98+
goodPosition =
99+
prior.displayParts[5]?.text === 'method' || (prior.displayParts[4]?.kind === 'keyword' && prior.displayParts[4].text === 'function')
100+
const { parts, gotMethodHit, hasOptionalParameters } = getParameterListParts(prior.displayParts)
101+
if (gotMethodHit) rawPartsOverride = hasOptionalParameters ? [...parts, { kind: '', text: ' ' }] : parts
102+
}
92103
const punctuationIndex = prior.displayParts.findIndex(({ kind }) => kind === 'punctuation')
93104
if (goodPosition && punctuationIndex !== 1) {
94105
const isParsableMethod = prior.displayParts
95106
// next is space
96107
.slice(punctuationIndex + 2)
97108
.map(({ text }) => text)
98109
.join('')
99-
.match(/\((.*)\) => /)
100-
if (isParsableMethod) {
110+
.match(/^\((.*)\) => /)
111+
if (rawPartsOverride || isParsableMethod) {
101112
let firstArgMeet = false
102-
const args = prior.displayParts
103-
.filter(({ kind }, index, array) => {
113+
const args = (
114+
rawPartsOverride ||
115+
prior.displayParts.filter(({ kind }, index, array) => {
104116
if (kind !== 'parameterName') return false
105117
if (array[index - 1]!.text === '(') {
106118
if (!firstArgMeet) {
107-
// bad parsing, as doesn't take second and more args
119+
// bad parsing, as it doesn't take second and more args
108120
firstArgMeet = true
109121
return true
110122
}
111123
return false
112124
}
113125
return true
114126
})
115-
.map(({ text }) => text)
116-
prior.documentation = [...(prior.documentation ?? []), { kind: 'text', text: `<!-- insert-func: ${args.join(',')}-->` }]
127+
).map(({ text }) => text)
128+
prior = {
129+
...prior,
130+
documentation: [...(prior.documentation ?? []), { kind: 'text', text: `<!-- insert-func: ${args.join(',')}-->` }],
131+
}
117132
}
118133
}
119134
}

typescript/src/isGoodPositionMethodCompletion.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { findChildContainingPosition, findChildContainingPositionMaxDepth } from
44
export const isGoodPositionBuiltinMethodCompletion = (ts: typeof tslib, sourceFile: tslib.SourceFile, position: number) => {
55
const importClauseCandidate = findChildContainingPositionMaxDepth(ts, sourceFile, position, 3)
66
if (importClauseCandidate && ts.isImportClause(importClauseCandidate)) return false
7-
let currentNode = findChildContainingPosition(ts, sourceFile, position)
7+
const textBeforePos = sourceFile.getFullText().slice(position - 1, position)
8+
let currentNode = findChildContainingPosition(ts, sourceFile, textBeforePos === ':' ? position - 1 : position)
89
if (currentNode) {
910
// const obj = { method() {}, arrow: () => {} }
1011
// type A = typeof obj["|"]
@@ -24,14 +25,15 @@ export const isGoodPositionMethodCompletion = (
2425
languageService: tslib.LanguageService,
2526
) => {
2627
if (!isGoodPositionBuiltinMethodCompletion(ts, sourceFile, position)) return false
27-
const { kind } = languageService.getQuickInfoAtPosition(fileName, position) ?? {}
28-
switch (kind) {
29-
case 'var':
30-
case 'let':
31-
case 'const':
32-
case 'alias':
33-
return false
34-
}
28+
// const { kind, displayParts } = languageService.getQuickInfoAtPosition(fileName, position) ?? {}
29+
// console.log('kind', kind, displayParts?.map(({ text }) => text).join(''))
30+
// switch (kind) {
31+
// case 'var':
32+
// case 'let':
33+
// case 'const':
34+
// case 'alias':
35+
// return false
36+
// }
3537
// TODO check for brace here
3638
return true
3739
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type tslib from 'typescript/lib/tsserverlibrary'
2+
3+
class DisplayPartKind {
4+
public static readonly functionName = 'functionName'
5+
public static readonly methodName = 'methodName'
6+
public static readonly parameterName = 'parameterName'
7+
public static readonly propertyName = 'propertyName'
8+
public static readonly punctuation = 'punctuation'
9+
public static readonly text = 'text'
10+
}
11+
12+
export function getParameterListParts(displayParts: ReadonlyArray<tslib.SymbolDisplayPart>) {
13+
const parts: tslib.SymbolDisplayPart[] = []
14+
let gotMethodHit = false
15+
let isInMethod = false
16+
let hasOptionalParameters = false
17+
let parenCount = 0
18+
let braceCount = 0
19+
20+
outer: for (let i = 0; i < displayParts.length; ++i) {
21+
const part = displayParts[i]!
22+
switch (part.kind) {
23+
case DisplayPartKind.methodName:
24+
case DisplayPartKind.functionName:
25+
case 'aliasName':
26+
case DisplayPartKind.text:
27+
case DisplayPartKind.propertyName:
28+
if (parenCount === 0 && braceCount === 0) {
29+
isInMethod = true
30+
gotMethodHit = true
31+
}
32+
break
33+
34+
case DisplayPartKind.parameterName:
35+
if (parenCount === 1 && braceCount === 0 && isInMethod) {
36+
// Only take top level paren names
37+
const next = displayParts[i + 1]
38+
// Skip optional parameters
39+
const nameIsFollowedByOptionalIndicator = next && next.text === '?'
40+
// Skip this parameter
41+
const nameIsThis = part.text === 'this'
42+
if (!nameIsFollowedByOptionalIndicator && !nameIsThis) {
43+
parts.push(part)
44+
}
45+
hasOptionalParameters = hasOptionalParameters || nameIsFollowedByOptionalIndicator!
46+
}
47+
break
48+
49+
case DisplayPartKind.punctuation:
50+
if (part.text === '(') {
51+
++parenCount
52+
} else if (part.text === ')') {
53+
--parenCount
54+
if (parenCount <= 0 && isInMethod) {
55+
break outer
56+
}
57+
} else if (part.text === '...' && parenCount === 1) {
58+
// Found rest parmeter. Do not fill in any further arguments
59+
hasOptionalParameters = true
60+
break outer
61+
} else if (part.text === '{') {
62+
++braceCount
63+
} else if (part.text === '}') {
64+
--braceCount
65+
}
66+
break
67+
}
68+
}
69+
70+
return { hasOptionalParameters, parts, gotMethodHit }
71+
}

typescript/test/completions.spec.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ test('Builtin method snippet banned positions', () => {
6666
const result = isGoodPositionBuiltinMethodCompletion(ts, getSourceFile(), pos)
6767
expect(result, i.toString()).toBeFalsy()
6868
}
69-
const insertTextEscaping = getCompletionsAtPosition(cursorPositions[1]!)!.entries[1].insertText!
69+
const insertTextEscaping = getCompletionsAtPosition(cursorPositions[1]!)!.entries[1]?.insertText!
7070
expect(insertTextEscaping).toEqual('m\\$1e\\$2thod')
7171
})
7272

73-
test('Additional banned positions for out method snippets', () => {
73+
test('Additional banned positions for our method snippets', () => {
7474
const cursorPositions = newFileContents(/* ts */ `
7575
const test = () => ({ method() {} })
7676
test({
@@ -86,6 +86,21 @@ test('Additional banned positions for out method snippets', () => {
8686
}
8787
})
8888

89+
test.only('Not banned positions for our method snippets', () => {
90+
const cursorPositions = newFileContents(/* ts */ `
91+
const test = () => ({ method() {} })
92+
const test2 = () => {}
93+
test({
94+
method: /*|*/
95+
})
96+
test2/*|*/
97+
`)
98+
for (const [i, pos] of cursorPositions.entries()) {
99+
const result = isGoodPositionMethodCompletion(ts, entrypoint, getSourceFile(), pos - 1, languageService)
100+
expect(result, i.toString()).toBeTruthy()
101+
}
102+
})
103+
89104
test.skip('Remove Useless Function Props', () => {
90105
const [pos] = newFileContents(/* ts */ `
91106
function fn() {}

0 commit comments

Comments
 (0)