Skip to content

Commit

Permalink
Merge pull request #125 from zardoy/develop
Browse files Browse the repository at this point in the history
  • Loading branch information
zardoy authored Apr 19, 2023
2 parents f436958 + 62f1cd5 commit df3bb76
Show file tree
Hide file tree
Showing 19 changed files with 258 additions and 102 deletions.
6 changes: 6 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ function Foo() {
}
```

`tsEssentialPlugins.methodSnippetsInsertText`:

Optionally resolve insertText of all completion at suggest trigger:

![method-snippets-insert-text](media/method-snippets-insert-text.png)

### Ambiguous Suggestions

Some variables like `Object` or `lodash` are common to access properties as well as call directly:
Expand Down
Binary file added media/method-snippets-insert-text.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"command": "insertNameOfCompletion",
"title": "Insert Name of Completion",
"category": "TS Essentials"
},
{
"command": "copyFullType",
"title": "Copy Full Type"
}
],
"keybindings": [
Expand Down
15 changes: 15 additions & 0 deletions src/configurationType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,15 @@ export type Configuration = {
* @default true
*/
enableMethodSnippets: boolean
/**
* Wether add insert text and detail to every function completion on each suggest trigger (instead of expanding method snippet after completion accept).
* This way you can enable support for method snippets in Vue files.
* `methodSnippets.replaceArguments` isn't supported for now.
* This is not enabled by default as it might be really slow in some cases.
* Recommended to try!
* @default disable
*/
methodSnippetsInsertText: 'disable' | 'only-local' | 'all'
/**
* ```ts
* const example = ({ a }, b?, c = 5, ...d) => { }
Expand Down Expand Up @@ -540,6 +549,12 @@ export type Configuration = {
* @default false
*/
'experiments.changeKindToFunction': boolean
/**
* Use workaround method for inserting name of TypeScript suggestion.
* If you move to next suggestion and then to previous, and then run *insert name of completion* via keybinding, name of **last resolved** completion will be inserted, so you might prefer to enable this setting. Also it makes this feature work in Vue.
* @default false
*/
'experiments.enableInsertNameOfSuggestionFix': boolean
/**
* Map *symbol - array of modules* to change sorting of imports - first available takes precedence in auto import code fixes (+ import all action)
*
Expand Down
36 changes: 20 additions & 16 deletions src/onCompletionAccepted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { getActiveRegularEditor } from '@zardoy/vscode-utils'
import { expandPosition } from '@zardoy/vscode-utils/build/position'
import { getExtensionSetting, registerExtensionCommand } from 'vscode-framework'
import { oneOf } from '@zardoy/utils'
import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes'
import { sendCommand } from './sendCommand'

export const onCompletionAcceptedOverride: { value: ((item: any) => void) | undefined } = { value: undefined }

export default (tsApi: { onCompletionAccepted }) => {
let inFlightMethodSnippetOperation: undefined | AbortController
let justAcceptedReturnKeywordSuggestion = false
let lastAcceptedAmbiguousMethodSnippetSuggestion: string | undefined
let onCompletionAcceptedOverride: ((item: any) => void) | undefined

// eslint-disable-next-line complexity
tsApi.onCompletionAccepted(async (item: vscode.CompletionItem & { document: vscode.TextDocument; tsEntry }) => {
if (onCompletionAcceptedOverride) {
onCompletionAcceptedOverride(item)
if (onCompletionAcceptedOverride.value) {
onCompletionAcceptedOverride.value(item)
onCompletionAcceptedOverride.value = undefined
return
}

Expand All @@ -38,10 +38,18 @@ export default (tsApi: { onCompletionAccepted }) => {

if (/* snippet is by vscode or by us to ignore pos */ typeof insertText !== 'object') {
const editor = getActiveRegularEditor()!
if (item.tsEntry.source) {

const documentation = typeof item.documentation === 'object' ? item.documentation.value : item.documentation
const dataMarker = '<!--tep '
if (!documentation?.startsWith(dataMarker)) return
const parsed = JSON.parse(documentation.slice(dataMarker.length, documentation.indexOf('e-->')))
const { methodSnippet: params, isAmbiguous, wordStartOffset } = parsed
const startPos = editor.selection.start
const acceptedWordStartOffset = wordStartOffset !== undefined && editor.document.getWordRangeAtPosition(startPos, /[\w\d]+/i)?.start
if (!oneOf(acceptedWordStartOffset, false, undefined) && wordStartOffset === editor.document.offsetAt(acceptedWordStartOffset)) {
await new Promise<void>(resolve => {
vscode.workspace.onDidChangeTextDocument(({ document }) => {
if (editor.document !== document) return
vscode.workspace.onDidChangeTextDocument(({ document, contentChanges }) => {
if (document !== editor.document || contentChanges.length === 0) return
resolve()
})
})
Expand All @@ -50,12 +58,9 @@ export default (tsApi: { onCompletionAccepted }) => {
})
}

const documentation = typeof item.documentation === 'object' ? item.documentation.value : item.documentation
const dataMarker = '<!--tep '
if (!documentation?.startsWith(dataMarker)) return
const parsed = JSON.parse(documentation.slice(dataMarker.length, documentation.indexOf('e-->')))
const { methodSnippet: params, isAmbiguous } = parsed
if (!params) return
// nextChar check also duplicated in completionEntryDetails for perf, but we need to run this check again with correct position
const nextChar = editor.document.getText(new vscode.Range(startPos, startPos.translate(0, 1)))
if (!params || ['(', '.', '`'].includes(nextChar)) return

if (isAmbiguous && lastAcceptedAmbiguousMethodSnippetSuggestion !== suggestionName) {
lastAcceptedAmbiguousMethodSnippetSuggestion = suggestionName
Expand Down Expand Up @@ -100,10 +105,9 @@ export default (tsApi: { onCompletionAccepted }) => {
async (_progress, token) => {
const accepted = await new Promise<boolean>(resolve => {
token.onCancellationRequested(() => {
onCompletionAcceptedOverride = undefined
resolve(false)
})
onCompletionAcceptedOverride = item => {
onCompletionAcceptedOverride.value = item => {
console.dir(item, { depth: 4 })
resolve(true)
}
Expand Down
53 changes: 48 additions & 5 deletions src/specialCommands.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as vscode from 'vscode'
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
import { getExtensionCommandId, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework'
import { getExtensionCommandId, getExtensionSetting, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework'
import { showQuickPick } from '@zardoy/vscode-utils/build/quickPick'
import _ from 'lodash'
import { compact } from '@zardoy/utils'
import { offsetPosition } from '@zardoy/vscode-utils/build/position'
import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes'
import { sendCommand } from './sendCommand'
import { tsRangeToVscode, tsRangeToVscodeSelection } from './util'
import { onCompletionAcceptedOverride } from './onCompletionAccepted'

export default () => {
registerExtensionCommand('removeFunctionArgumentsTypesInSelection', async () => {
Expand Down Expand Up @@ -238,11 +240,52 @@ export default () => {
await vscode.commands.executeCommand(preview ? 'acceptRenameInputWithPreview' : 'acceptRenameInput')
})

registerExtensionCommand('insertNameOfCompletion', async () => {
registerExtensionCommand('insertNameOfCompletion', async (_, { insertMode } = {}) => {
const editor = vscode.window.activeTextEditor
if (!editor) return
const result = await sendCommand<RequestResponseTypes['getLastResolvedCompletion']>('getLastResolvedCompletion')
if (!result) return
await editor.insertSnippet(new vscode.SnippetString().appendText(result.name))
if (!getExtensionSetting('experiments.enableInsertNameOfSuggestionFix')) {
const result = await sendCommand<RequestResponseTypes['getLastResolvedCompletion']>('getLastResolvedCompletion')
if (!result) return
const position = editor.selection.active
const range = result.range ? tsRangeToVscode(editor.document, result.range) : editor.document.getWordRangeAtPosition(position)
await editor.insertSnippet(
new vscode.SnippetString().appendText(result.name),
(insertMode || vscode.workspace.getConfiguration().get('editor.suggest.insertMode')) === 'replace' ? range : range?.with(undefined, position),
)
return
}

onCompletionAcceptedOverride.value = () => {}
const { ranges, text } = await new Promise<{ text: string; ranges: vscode.Range[] }>(resolve => {
vscode.workspace.onDidChangeTextDocument(({ document, contentChanges }) => {
if (document !== editor.document || contentChanges.length === 0) return
const ranges = contentChanges.map(
change => new vscode.Range(change.range.start, offsetPosition(document, change.range.start, change.text.length)),
)
resolve({ ranges, text: contentChanges[0]!.text })
})
void vscode.commands.executeCommand('acceptSelectedSuggestion')
})
const needle = ['(', ': '].find(needle => text.includes(needle))
if (!needle) return
const cleanedText = text.slice(0, text.indexOf(needle))
await editor.edit(
e => {
for (const range of ranges) {
e.replace(range, cleanedText)
}
},
{
undoStopBefore: false,
undoStopAfter: false,
},
)
})

registerExtensionCommand('copyFullType', async () => {
const response = await sendCommand<RequestResponseTypes['getFullType']>('getFullType')
if (!response) return
const { text } = response
await vscode.env.clipboard.writeText(text)
})
}
11 changes: 9 additions & 2 deletions typescript/src/completionEntryDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import constructMethodSnippet from './constructMethodSnippet'
import { RequestResponseTypes } from './ipcTypes'
import namespaceAutoImports from './namespaceAutoImports'
import { GetConfig } from './types'
import { wordStartAtPos } from './utils'

export const lastResolvedCompletion = {
value: undefined as undefined | RequestResponseTypes['getLastResolvedCompletion'],
Expand All @@ -16,7 +17,7 @@ export default function completionEntryDetails(
{ enableMethodCompletion, completionsSymbolMap }: PrevCompletionsAdditionalData,
): ts.CompletionEntryDetails | undefined {
const [fileName, position, entryName, formatOptions, source, preferences, data] = inputArgs
lastResolvedCompletion.value = { name: entryName }
lastResolvedCompletion.value = { name: entryName, range: prevCompletionsMap[entryName]?.range }
const program = languageService.getProgram()
const sourceFile = program?.getSourceFile(fileName)
if (!program || !sourceFile) return
Expand Down Expand Up @@ -49,6 +50,7 @@ export default function completionEntryDetails(
prior.displayParts = [{ kind: 'text', text: detailPrepend }, ...prior.displayParts]
}
if (!prior) return
// might be incorrect: write [].entries() -> []|.entries|() -> []./*position*/e
const nextChar = sourceFile.getFullText().slice(position, position + 1)

if (enableMethodCompletion && c('enableMethodSnippets') && !['(', '.', '`'].includes(nextChar)) {
Expand All @@ -59,7 +61,12 @@ export default function completionEntryDetails(
}
const methodSnippet = constructMethodSnippet(languageService, sourceFile, position, symbol, c, resolveData)
if (methodSnippet) {
const data = JSON.stringify({ methodSnippet, isAmbiguous: resolveData.isAmbiguous })
const wordStartOffset = source ? wordStartAtPos(sourceFile.getFullText(), position) : undefined
const data = JSON.stringify({
methodSnippet,
isAmbiguous: resolveData.isAmbiguous,
wordStartOffset,
})
prior.documentation = [{ kind: 'text', text: `<!--tep ${data} e-->` }, ...(prior.documentation ?? [])]
}
}
Expand Down
46 changes: 0 additions & 46 deletions typescript/src/completions/changeKindToFunction.ts

This file was deleted.

70 changes: 70 additions & 0 deletions typescript/src/completions/functionCompletions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { oneOf } from '@zardoy/utils'
import constructMethodSnippet from '../constructMethodSnippet'
import { insertTextAfterEntry } from '../utils'
import { sharedCompletionContext } from './sharedContext'

export default (entries: ts.CompletionEntry[]) => {
const { languageService, c, sourceFile, position } = sharedCompletionContext

const methodSnippetInsertTextMode = c('methodSnippetsInsertText')
const enableResolvingInsertText = c('enableMethodSnippets') && methodSnippetInsertTextMode !== 'disable'
const changeKindToFunction = c('experiments.changeKindToFunction')

if (!enableResolvingInsertText && !changeKindToFunction) return

const typeChecker = languageService.getProgram()!.getTypeChecker()!
// let timeSpend = 0
const newEntries = entries.map(entry => {
const patch = (): ts.CompletionEntry | undefined => {
const { kind, symbol } = entry
if (
!enableResolvingInsertText &&
!oneOf(
kind,
ts.ScriptElementKind.alias,
ts.ScriptElementKind.memberVariableElement,
ts.ScriptElementKind.variableElement,
ts.ScriptElementKind.localVariableElement,
ts.ScriptElementKind.constElement,
ts.ScriptElementKind.variableElement,
)
) {
return
}
if (methodSnippetInsertTextMode === 'only-local' && entry.source) return
if (!symbol) return
const { valueDeclaration = symbol.declarations?.[0] } = symbol
if (!valueDeclaration) return

// const dateNow = Date.now()
if (enableResolvingInsertText) {
const resolveData = {} as { isAmbiguous: boolean }
const methodSnippet = constructMethodSnippet(languageService, sourceFile, position, symbol, c, resolveData)
if (!methodSnippet || resolveData.isAmbiguous) return
return {
...entry,
insertText: insertTextAfterEntry(entry, `(${methodSnippet.map((x, i) => `$\{${i + 1}:${x}}`).join(', ')})`),
labelDetails: {
detail: `(${methodSnippet.join(', ')})`,
description: ts.displayPartsToString(entry.sourceDisplay),
},
kind: changeKindToFunction ? ts.ScriptElementKind.functionElement : entry.kind,
isSnippet: true,
}
}
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, valueDeclaration)
const signatures = typeChecker.getSignaturesOfType(type, ts.SignatureKind.Call)
// timeSpend += Date.now() - dateNow
if (signatures.length === 0) return

return { ...entry, kind: ts.ScriptElementKind.functionElement }
}

return patch() ?? entry
})

// remove logging once stable
// console.log('changeKindToFunction time:', timeSpend)

return newEntries
}
2 changes: 1 addition & 1 deletion typescript/src/completions/localityBonus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default (entries: ts.CompletionEntry[]) => {
// eslint-disable-next-line prefer-destructuring
const symbol: ts.Symbol | undefined = entry['symbol']
if (!symbol) return
const { valueDeclaration } = symbol
const { valueDeclaration = symbol.declarations?.[0] } = symbol
if (!valueDeclaration) return
if (valueDeclaration.getSourceFile().fileName !== sourceFile.fileName) return -1
return valueDeclaration.pos
Expand Down
8 changes: 4 additions & 4 deletions typescript/src/completions/objectLiteralCompletions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getFullTypeChecker, isTs5 } from '../utils'
import { getFullTypeChecker, insertTextAfterEntry, isTs5 } from '../utils'
import { sharedCompletionContext } from './sharedContext'

export default (prior: ts.CompletionInfo): ts.CompletionEntry[] | void => {
Expand All @@ -22,8 +22,8 @@ export default (prior: ts.CompletionInfo): ts.CompletionEntry[] | void => {
if (!objType) return
oldProperties = getAllPropertiesOfType(objType, typeChecker)
}
// eslint-disable-next-line unicorn/no-useless-spread
for (const entry of [...entries]) {
const clonedEntries = [...entries]
for (const entry of clonedEntries) {
let type: ts.Type | undefined
if (!isTs5()) {
const property = oldProperties!.find(property => property.name === entry.name)
Expand Down Expand Up @@ -66,7 +66,7 @@ export default (prior: ts.CompletionInfo): ts.CompletionEntry[] | void => {
const insertSnippetVariant = completingStyleMap.find(([, detector]) => detector(type!, typeChecker))?.[0] ?? fallbackSnippet
if (!insertSnippetVariant) continue
const [insertSnippetText, insertSnippetPreview] = typeof insertSnippetVariant === 'function' ? insertSnippetVariant() : insertSnippetVariant
const insertText = entry.name + insertSnippetText
const insertText = insertTextAfterEntry(entry, insertSnippetText)
const index = entries.indexOf(entry)
entries.splice(index + (keepOriginal === 'before' ? 1 : 0), keepOriginal === 'remove' ? 1 : 0, {
...entry,
Expand Down
Loading

0 comments on commit df3bb76

Please sign in to comment.