diff --git a/README.MD b/README.MD index 06ecc6e5..404f70c6 100644 --- a/README.MD +++ b/README.MD @@ -228,6 +228,34 @@ Some settings examples: > Note: changeSorting might not preserve sorting of other existing suggestions which not defined by rules, there is WIP > Also I'm thinking of making it learn and syncing of most-used imports automatically +## Rename Features + +There is builtin mechanism to rename variable occurrences in strings & comments, it is disabled in VS Code without a way to enable it. + +However this extension also has builtin keybinding `Ctrl+Shift+Enter` that can be pressed when input box is visible to enable aforementioned behavior for renaming with preview. + +But note renaming in strings & comments will happen only for files in which variable is actually referenced. + +You can add this to `keybindings.json` to disable previewing before renaming: + +```js +{ + "key": "ctrl+shift+enter", + "command": "tsEssentialPlugins.acceptRenameWithParams", + "args": { + "strings": true, + "comments": true, + // "preview": true // true by default + // "alias": true // you can also specify here wether to introduce alias on rename if applicable (overriding global setting) + }, + "when": "renameInputVisible" +} +``` + +Another options that is accepted is + +> Note: VS Code has builtin setting to disable introducing aliases (e.g. for imports & object properties) + ## Special Commands List ### Go to / Select Nodes by Kind diff --git a/package.json b/package.json index 2b7f3efd..8400c331 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,19 @@ "category": "TS Essentials" } ], + "keybindings": [ + { + "key": "ctrl+shift+enter", + "mac": "cmd+shift+enter", + "command": "tsEssentialPlugins.acceptRenameWithParams", + "args": { + "strings": true, + "comments": true, + "preview": true + }, + "when": "renameInputVisible && editorLangId =~ /javascript|javascriptreact|typescript|typescriptreact|vue/" + } + ], "typescriptServerPlugins": [ { "name": "typescript-essential-plugins", @@ -105,7 +118,7 @@ "type-fest": "^2.13.1", "typed-jsonfile": "^0.2.1", "typescript": "^4.9.3", - "vitest": "^0.25.3", + "vitest": "^0.26.0", "vscode-manifest": "^0.0.4" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f71054e5..7dde3fa7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,7 +52,7 @@ importers: typed-jsonfile: ^0.2.1 typescript: ^4.9.3 unleashed-typescript: ^1.3.0 - vitest: ^0.25.3 + vitest: ^0.26.0 vscode-framework: ^0.0.18 vscode-manifest: ^0.0.4 vscode-uri: ^3.0.6 @@ -105,7 +105,7 @@ importers: type-fest: 2.13.1 typed-jsonfile: 0.2.1 typescript: 4.9.3 - vitest: 0.25.3 + vitest: 0.26.3 vscode-manifest: 0.0.4 typescript: @@ -595,6 +595,7 @@ packages: /@types/chai/4.3.3: resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==} + dev: false /@types/chai/4.3.4: resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} @@ -1259,7 +1260,6 @@ packages: /buffer-from/1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: false /buffer-indexof-polyfill/1.0.2: resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} @@ -1361,6 +1361,20 @@ packages: loupe: 2.3.4 pathval: 1.1.1 type-detect: 4.0.8 + dev: false + + /chai/4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.4 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true /chainsaw/0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} @@ -1736,6 +1750,14 @@ packages: engines: {node: '>=0.12'} dependencies: type-detect: 4.0.8 + dev: false + + /deep-eql/4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true /deep-equal/1.0.1: resolution: {integrity: sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=} @@ -3430,7 +3452,6 @@ packages: /jsonc-parser/3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - dev: false /jsonfile/6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -3786,6 +3807,15 @@ packages: hasBin: true dev: false + /mlly/1.1.0: + resolution: {integrity: sha512-cwzBrBfwGC1gYJyfcy8TcZU1f+dbH/T+TuOhtYP2wLv/Fb51/uV7HJQfBPtEupZ2ORLRU1EKFS/QfS3eo9+kBQ==} + dependencies: + acorn: 8.8.1 + pathe: 1.1.0 + pkg-types: 1.0.1 + ufo: 1.0.1 + dev: true + /mocha/10.0.0: resolution: {integrity: sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==} engines: {node: '>= 14.0.0'} @@ -4166,6 +4196,14 @@ packages: engines: {node: '>=8'} dev: false + /pathe/0.2.0: + resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} + dev: true + + /pathe/1.1.0: + resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + dev: true + /pathval/1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} @@ -4209,6 +4247,14 @@ packages: find-up: 5.0.0 dev: false + /pkg-types/1.0.1: + resolution: {integrity: sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.1.0 + pathe: 1.1.0 + dev: true + /playwright/1.14.1: resolution: {integrity: sha512-JYNjhwWcfsBkg0FMGLbFO9e58FVdmICE4k97/glIQV7cBULL7oxNjRQC7Ffe+Y70XVNnP0HSJLaA0W5SukyftQ==} engines: {node: '>=12'} @@ -4661,6 +4707,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map-support/0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + /source-map/0.5.7: resolution: {integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=} engines: {node: '>=0.10.0'} @@ -4833,8 +4886,8 @@ packages: engines: {node: '>=10.14.2'} dev: false - /strip-literal/0.4.2: - resolution: {integrity: sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==} + /strip-literal/1.0.0: + resolution: {integrity: sha512-5o4LsH1lzBzO9UFH63AJ2ad2/S2AVx6NtjOcaz+VTT2h1RiRvbipW72z8M/lxEhcPHDBQwpDrnTF7sXy/7OwCQ==} dependencies: acorn: 8.8.1 dev: true @@ -5137,6 +5190,10 @@ packages: engines: {node: '>=4.2.0'} hasBin: true + /ufo/1.0.1: + resolution: {integrity: sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==} + dev: true + /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -5248,6 +5305,27 @@ packages: engines: {node: '>= 0.8'} dev: false + /vite-node/0.26.3_@types+node@16.18.3: + resolution: {integrity: sha512-Te2bq0Bfvq6XiO718I+1EinMjpNYKws6SNHKOmVbILAQimKoZKDd+IZLlkaYcBXPpK3HFe2U80k8Zw+m3w/a2w==} + engines: {node: '>=v14.16.0'} + hasBin: true + dependencies: + debug: 4.3.4 + mlly: 1.1.0 + pathe: 0.2.0 + source-map: 0.6.1 + source-map-support: 0.5.21 + vite: 3.2.4_@types+node@16.18.3 + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite/3.2.4_@types+node@16.18.3: resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -5282,8 +5360,8 @@ packages: fsevents: 2.3.2 dev: true - /vitest/0.25.3: - resolution: {integrity: sha512-/UzHfXIKsELZhL7OaM2xFlRF8HRZgAHtPctacvNK8H4vOcbJJAMEgbWNGSAK7Y9b1NBe5SeM7VTuz2RsTHFJJA==} + /vitest/0.26.3: + resolution: {integrity: sha512-FmHxU9aUCxTi23keF3vxb/Qp0lYXaaJ+jRLGOUmMS3qVTOJvgGE+f1VArupA6pEhaG2Ans4X+zV9dqM5WISMbg==} engines: {node: '>=v14.16.0'} hasBin: true peerDependencies: @@ -5304,20 +5382,21 @@ packages: jsdom: optional: true dependencies: - '@types/chai': 4.3.3 + '@types/chai': 4.3.4 '@types/chai-subset': 1.3.3 '@types/node': 16.18.3 acorn: 8.8.1 acorn-walk: 8.2.0 - chai: 4.3.6 + chai: 4.3.7 debug: 4.3.4 local-pkg: 0.4.2 source-map: 0.6.1 - strip-literal: 0.4.2 + strip-literal: 1.0.0 tinybench: 2.3.1 tinypool: 0.3.0 tinyspy: 1.0.2 vite: 3.2.4_@types+node@16.18.3 + vite-node: 0.26.3_@types+node@16.18.3 transitivePeerDependencies: - less - sass diff --git a/src/configurationType.ts b/src/configurationType.ts index d19c148c..90c0564f 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -63,10 +63,6 @@ export type Configuration = { * @default false */ enableVueSupport: boolean - /** - * @default true - */ - vueSpecificImprovements: boolean /** * Temporary setting to enable loading config from other locations (also to expose plugin) */ diff --git a/src/extension.ts b/src/extension.ts index afcff6f2..92addb07 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -14,6 +14,7 @@ import apiCommands from './apiCommands' import onCompletionAccepted from './onCompletionAccepted' import specialCommands from './specialCommands' import vueVolarSupport from './vueVolarSupport' +import moreCompletions from './moreCompletions' export const activateTsPlugin = (tsApi: { configurePlugin; onCompletionAccepted }) => { let webWaitingForConfigSync = false @@ -65,6 +66,7 @@ export const activateTsPlugin = (tsApi: { configurePlugin; onCompletionAccepted } experimentalPostfixes() + moreCompletions() void registerEmmet() webImports() apiCommands() diff --git a/src/moreCompletions.ts b/src/moreCompletions.ts new file mode 100644 index 00000000..5441cfd0 --- /dev/null +++ b/src/moreCompletions.ts @@ -0,0 +1,29 @@ +import * as vscode from 'vscode' +import { defaultJsSupersetLangsWithVue } from '@zardoy/vscode-utils/build/langs' + +export default () => { + vscode.languages.registerCompletionItemProvider( + defaultJsSupersetLangsWithVue, + { + provideCompletionItems(document, position, token, context) { + const regex = /\/\/@?[\w-]*/ + let range = document.getWordRangeAtPosition(position, regex) + if (!range) return + const rangeText = document.getText(range) + if (rangeText !== document.lineAt(position).text.trim()) { + return + } + + range = range.with(range.start.translate(0, 2), range.end) + const tsDirectives = ['@ts-format-ignore-line', '@ts-format-ignore-region', '@ts-format-ignore-endregion'] + return tsDirectives.map((directive, i) => { + const completionItem = new vscode.CompletionItem(directive, vscode.CompletionItemKind.Snippet) + completionItem.range = range + completionItem.sortText = `z${i}` + return completionItem + }) + }, + }, + '@', + ) +} diff --git a/src/specialCommands.ts b/src/specialCommands.ts index 186ac944..6ad75704 100644 --- a/src/specialCommands.ts +++ b/src/specialCommands.ts @@ -265,6 +265,25 @@ export default () => { await vscode.workspace.applyEdit(edit) }) + registerExtensionCommand('acceptRenameWithParams' as any, async (_, { preview = false, comments = null, strings = null, alias = null } = {}) => { + const editor = vscode.window.activeTextEditor + if (!editor) return + const { + document, + selection: { active: position }, + } = editor + await sendCommand('acceptRenameWithParams', { + document, + position, + inputOptions: { + alias, + comments, + strings, + } satisfies RequestOptionsTypes['acceptRenameWithParams'], + }) + await vscode.commands.executeCommand(preview ? 'acceptRenameInputWithPreview' : 'acceptRenameInput') + }) + // its actually a code action, but will be removed from there soon vscode.languages.registerCodeActionsProvider(defaultJsSupersetLangsWithVue, { async provideCodeActions(document, range, context, token) { diff --git a/typescript/src/codeFixes.ts b/typescript/src/codeFixes.ts index 02e9a9d4..1f8eb9c7 100644 --- a/typescript/src/codeFixes.ts +++ b/typescript/src/codeFixes.ts @@ -65,6 +65,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, } finally { tsFull.codefix.createCodeFixAction = oldCreateCodeFixAction } + // todo remove when 5.0 is released after 3 months // #region fix builtin codefixes/refactorings prior.forEach(fix => { if (fix.fixName === 'fixConvertConstToLet') { diff --git a/typescript/src/completions/arrayMethods.ts b/typescript/src/completions/arrayMethods.ts index ddaf35cc..fd289055 100644 --- a/typescript/src/completions/arrayMethods.ts +++ b/typescript/src/completions/arrayMethods.ts @@ -1,6 +1,6 @@ import { GetConfig } from '../types' import { findChildContainingPosition, getLineTextBeforePos } from '../utils' -import { singular } from 'pluralize' +import pluralize from 'pluralize' const arrayMethodsToPatch = [ 'forEach', @@ -31,7 +31,7 @@ export default (entries: ts.CompletionEntry[], position: number, sourceFile: ts. if (!nodeBeforeDot) return const cleanSourceText = getItemNameFromNode(nodeBeforeDot)?.replace(/^(?:all)?(.+?)(?:List)?$/, '$1') - let inferredName = cleanSourceText && singular(cleanSourceText) + let inferredName = cleanSourceText && pluralize.singular(cleanSourceText) const defaultItemName = c('arrayMethodsSnippets.defaultItemName') // both can be undefined if (inferredName === cleanSourceText) { diff --git a/typescript/src/completions/switchCaseExcludeCovered.ts b/typescript/src/completions/switchCaseExcludeCovered.ts index 78552ac3..0104b7ee 100644 --- a/typescript/src/completions/switchCaseExcludeCovered.ts +++ b/typescript/src/completions/switchCaseExcludeCovered.ts @@ -2,6 +2,7 @@ import { oneOf } from '@zardoy/utils' import { cleanupEntryName } from '../utils' // implementation not even ideal, but it just works for string & enums, which are used in 99% cases +// todo remove when 5.0 is released after 3 months export default (entries: ts.CompletionEntry[], position: number, sourceFile: ts.SourceFile, leftNode: ts.Node) => { if (!leftNode.parent?.parent) return let nodeComp = leftNode diff --git a/typescript/src/completionsAtPosition.ts b/typescript/src/completionsAtPosition.ts index ebc86c88..07944516 100644 --- a/typescript/src/completionsAtPosition.ts +++ b/typescript/src/completionsAtPosition.ts @@ -216,7 +216,9 @@ export const getCompletionsAtPosition = ( prior.entries = arrayMethods(prior.entries, position, sourceFile, c) ?? prior.entries prior.entries = jsdocDefault(prior.entries, position, sourceFile, languageService) ?? prior.entries - if ((fileName.endsWith('.vue.ts') || fileName.endsWith('.vue.js')) && c('vueSpecificImprovements') && exactNode) { + // #region Vue (Volar) specific + const isVueFile = fileName.endsWith('.vue.ts') || fileName.endsWith('.vue.js') + if (isVueFile && exactNode) { let node = ts.isIdentifier(exactNode) ? exactNode.parent : exactNode if (ts.isPropertyAssignment(node)) node = node.parent if ( @@ -228,6 +230,7 @@ export const getCompletionsAtPosition = ( prior.entries = prior.entries.filter(({ name, kind }) => kind === ts.ScriptElementKind.warning || !name.startsWith('__')) } } + // #endregion prior.entries = addSourceDefinition(prior.entries, prevCompletionsMap, c) ?? prior.entries @@ -357,22 +360,23 @@ const patchBuiltinMethods = (c: GetConfig, languageService: ts.LanguageService, // TODO! when file name without with half-ending is typed it doesn't these completions! (seems ts bug, but probably can be fixed here) // e.g. /styles.css import './styles.c|' - no completions const oldGetSupportedExtensions = tsFull.getSupportedExtensions - //@ts-expect-error monkey patch - tsFull.getSupportedExtensions = (options, extraFileExtensions) => { - addFileExtensions ??= getAddFileExtensions() - // though I extensions could be just inlined as is - return oldGetSupportedExtensions( - options, - extraFileExtensions?.length - ? extraFileExtensions - : addFileExtensions.map(ext => ({ - extension: ext, - isMixedContent: true, - scriptKind: ts.ScriptKind.Deferred, - })), - ) - } + Object.defineProperty(tsFull, 'getSupportedExtensions', { + value: (options, extraFileExtensions) => { + addFileExtensions ??= getAddFileExtensions() + // though I extensions could be just inlined as is + return oldGetSupportedExtensions( + options, + extraFileExtensions?.length + ? extraFileExtensions + : addFileExtensions.map(ext => ({ + extension: ext, + isMixedContent: true, + scriptKind: ts.ScriptKind.Deferred, + })), + ) + }, + }) return () => { - tsFull.getSupportedExtensions = oldGetSupportedExtensions + Object.defineProperty(tsFull, 'getSupportedExtensions', { value: oldGetSupportedExtensions }) } } diff --git a/typescript/src/decorateFormatFeatures.ts b/typescript/src/decorateFormatFeatures.ts new file mode 100644 index 00000000..f719a761 --- /dev/null +++ b/typescript/src/decorateFormatFeatures.ts @@ -0,0 +1,59 @@ +import { GetConfig } from './types' +import { patchMethod } from './utils' + +export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig) => { + // const oldGetAllRules = tsFull.formatting.getAllRules; + // tsFull.formatting.getAllRules = () => { + // } + + const isFormattingLineIgnored = (sourceFile: ts.SourceFile, position: number) => { + // const sourceFile = languageService.getProgram()!.getSourceFile(fileName)! + const fullText = sourceFile.getFullText() + // check that lines before line are not ignored + const linesBefore = fullText.slice(0, position).split('\n') + if (linesBefore[linesBefore.length - 2]?.trim() === '//@ts-format-ignore-line') { + return true + } + + let isInsideIgnoredRegion = false + for (const line of linesBefore) { + if (line.trim() === '//@ts-format-ignore-region') { + isInsideIgnoredRegion = true + } + if (line.trim() === '//@ts-format-ignore-endregion') { + isInsideIgnoredRegion = false + } + } + return isInsideIgnoredRegion + } + // proxy.getFormattingEditsAfterKeystroke = (fileName, position, key, options) => { + // // if (isFormattingLineIgnored(fileName, position)) { + // // return [] + // // } + // return languageService.getFormattingEditsAfterKeystroke(fileName, position, key, options) + // } + // proxy.getFormattingEditsForDocument = (fileName, options) => { + // return [] + // } + const toPatchFormatMethods = ['formatSelection', 'formatOnOpeningCurly', 'formatOnClosingCurly', 'formatOnSemicolon', 'formatOnEnter'] + for (const toPatchFormatMethod of toPatchFormatMethods) { + patchMethod(tsFull.formatting, toPatchFormatMethod as any, oldFn => (...args) => { + const result = oldFn(...args) + const sourceFile = args.find(arg => ts.isSourceFile(arg as any)) + return result.filter(({ span }) => { + if (isFormattingLineIgnored(sourceFile as ts.SourceFile, span.start)) { + return false + } + return true + }) + }) + } + // proxy.getFormattingEditsForRange = (fileName, start, end, options) => { + // return languageService.getFormattingEditsForRange(fileName, start, end, options).filter(({ span }) => { + // if (isFormattingLineIgnored(fileName, span.start)) { + // return false + // } + // return true + // }) + // } +} diff --git a/typescript/src/decorateProxy.ts b/typescript/src/decorateProxy.ts index 4483f6ea..8ae8aa71 100644 --- a/typescript/src/decorateProxy.ts +++ b/typescript/src/decorateProxy.ts @@ -1,5 +1,5 @@ import { getCompletionsAtPosition, PrevCompletionMap, PrevCompletionsAdditionalData } from './completionsAtPosition' -import { TriggerCharacterCommand } from './ipcTypes' +import { RequestOptionsTypes, TriggerCharacterCommand } from './ipcTypes' import { getNavTreeItems } from './getPatchedNavTree' import decorateCodeActions from './codeActions/decorateProxy' import decorateSemanticDiagnostics from './semanticDiagnostics' @@ -12,6 +12,7 @@ import completionEntryDetails from './completionEntryDetails' import { GetConfig } from './types' import lodashGet from 'lodash.get' import decorateWorkspaceSymbolSearch from './workspaceSymbolSearch' +import decorateFormatFeatures from './decorateFormatFeatures' /** @internal */ export const thisPluginMarker = '__essentialPluginsMarker__' @@ -25,6 +26,10 @@ export const getInitialProxy = (languageService: ts.LanguageService, proxy = Obj return proxy } +export const overrideRequestPreferences = { + rename: undefined as undefined | RequestOptionsTypes['acceptRenameWithParams'], +} + export const decorateLanguageService = ( info: ts.server.PluginCreateInfo, existingProxy: ts.LanguageService | undefined, @@ -109,6 +114,24 @@ export const decorateLanguageService = ( decorateReferences(proxy, languageService, c) decorateDocumentHighlights(proxy, languageService, c) decorateWorkspaceSymbolSearch(proxy, languageService, c, languageServiceHost) + decorateFormatFeatures(proxy, languageService, c) + proxy.findRenameLocations = (fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename) => { + if (overrideRequestPreferences.rename) { + try { + const { comments, strings, alias } = overrideRequestPreferences.rename + return languageService.findRenameLocations( + fileName, + position, + strings ?? findInStrings, + comments ?? findInComments, + alias ?? providePrefixAndSuffixTextForRename, + ) + } finally { + overrideRequestPreferences.rename = undefined + } + } + return languageService.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename) + } if (pluginSpecificSyntaxServerConfigCheck) { if (!__WEB__) { diff --git a/typescript/src/ipcTypes.ts b/typescript/src/ipcTypes.ts index 80f34d7d..eac5a521 100644 --- a/typescript/src/ipcTypes.ts +++ b/typescript/src/ipcTypes.ts @@ -10,6 +10,7 @@ export const triggerCharacterCommands = [ 'getRangeOfSpecialValue', 'turnArrayIntoObject', 'getFixAllEdits', + 'acceptRenameWithParams', ] as const export type TriggerCharacterCommand = typeof triggerCharacterCommands[number] @@ -57,6 +58,11 @@ export type RequestOptionsTypes = { range: [number, number] selectedKeyName?: string } + acceptRenameWithParams: { + comments: boolean + strings: boolean + alias: boolean + } } // export type EmmetResult = { diff --git a/typescript/src/specialCommands/handle.ts b/typescript/src/specialCommands/handle.ts index ef7967e1..aceed23a 100644 --- a/typescript/src/specialCommands/handle.ts +++ b/typescript/src/specialCommands/handle.ts @@ -1,5 +1,6 @@ import { compact } from '@zardoy/utils' import postfixesAtPosition from '../completions/postfixesAtPosition' +import { overrideRequestPreferences } from '../decorateProxy' import { NodeAtPositionResponse, RequestOptionsTypes, RequestResponseTypes, TriggerCharacterCommand, triggerCharacterCommands } from '../ipcTypes' import { findChildContainingExactPosition, findChildContainingPosition, getNodePath } from '../utils' import getEmmetCompletions from './emmet' @@ -181,6 +182,14 @@ export default ( return } } + if (specialCommand === 'acceptRenameWithParams') { + changeType(specialCommandArg) + overrideRequestPreferences.rename = specialCommandArg + return { + entries: [], + typescriptEssentialsResponse: undefined, + } + } if (specialCommand === 'pickAndInsertFunctionArguments') { // const sourceFile = (info.languageService as ReturnType).getProgram()!.getSourceFile(fileName)! // if (!sourceFile.identifiers) throw new Error('TS: no exposed identifiers map') diff --git a/typescript/src/utils.ts b/typescript/src/utils.ts index e304a19d..77caa0d7 100644 --- a/typescript/src/utils.ts +++ b/typescript/src/utils.ts @@ -185,3 +185,15 @@ export function approveCast node is ts.Node> if (!oneOfTest) throw new Error('Tests are not provided') return oneOfTest.some(test => test(node)) } + +export const patchMethod = (obj: T, method: K, overriden: (oldMethod: T[K]) => T[K]) => { + const oldValue = obj[method] + Object.defineProperty(obj, method, { + value: overriden(oldValue), + }) + return () => { + Object.defineProperty(obj, method, { + value: oldValue, + }) + } +} diff --git a/typescript/test/completions.spec.ts b/typescript/test/completions.spec.ts index aa0e984b..af80e750 100644 --- a/typescript/test/completions.spec.ts +++ b/typescript/test/completions.spec.ts @@ -12,6 +12,9 @@ import { createRequire } from 'module' import { findChildContainingPosition } from '../src/utils' import handleCommand from '../src/specialCommands/handle' import _ from 'lodash' +import decorateFormatFeatures from '../src/decorateFormatFeatures' + +// TODO rename file to plugin.spec.ts or move other tests const require = createRequire(import.meta.url) //@ts-ignore plugin expect it to set globallly @@ -499,3 +502,34 @@ test('In Keyword Completions', () => { } `) }) + +test('Format ignore', () => { + decorateFormatFeatures(languageService, { ...languageService }, defaultConfigFunc) + newFileContents(/* ts */ ` +const a = { + //@ts-format-ignore-region + a: 1, + a1: 2, + //@ts-format-ignore-endregion + b: 3, + //@ts-format-ignore-line + c: 4, +}`) + const edits = languageService.getFormattingEditsForRange(entrypoint, 0, files[entrypoint]!.length, ts.getDefaultFormatCodeSettings()) + // const sourceFile = languageService.getProgram()!.getSourceFile(entrypoint)! + // const text = sourceFile.getFullText() + // edits.forEach(edit => { + // console.log(text.slice(0, edit.span.start) + '<<<' + edit.newText + '>>>' + text.slice(edit.span.start + edit.span.length)) + // }) + expect(edits).toMatchInlineSnapshot(/* json */ ` + [ + { + "newText": " ", + "span": { + "length": 2, + "start": 108, + }, + }, + ] + `) +})